using System.Runtime.Versioning;
using Kompas6API5;
using Kompas6Constants;
using Kompas6Constants3D;

namespace Kompas.Mcp.Core.Modeling;

/// <summary>PartModeler: создание эскизов и добавление 2D-примитивов.</summary>
[SupportedOSPlatform("windows")]
public sealed partial class PartModeler
{
    /// <summary>Создать эскиз на базовой плоскости и открыть его на редактирование. Возвращает id эскиза.</summary>
    public Task<int> OpenSketchAsync(BasePlane plane, string? name = null, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var part = GetTopPart();
            var planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(plane)) as ksEntity
                ?? throw new InvalidOperationException("Не удалось получить базовую плоскость.");
            return CreateSketchOn(part, planeEntity, name);
        }, ct);

    /// <summary>
    /// Создать эскиз на плоской грани детали, найденной по точке <paramref name="x"/>,<paramref name="y"/>,<paramref name="z"/>
    /// (мировые координаты модели, мм), и открыть его на редактирование. Возвращает id эскиза.
    /// Это ключ к итеративному циклу «построил → выбрал грань → новый эскиз на ней».
    /// </summary>
    public Task<int> OpenSketchOnFaceAsync(double x, double y, double z, string? name = null, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var part = GetTopPart();
            var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection
                ?? throw new InvalidOperationException("Не удалось получить коллекцию граней.");
            // Оставляем в коллекции только грани, проходящие через указанную точку.
            faces.SelectByPoint(x, y, z);
            if (faces.GetCount() == 0)
                throw new InvalidOperationException(
                    $"В точке ({x}, {y}, {z}) не найдено грани. Сделайте снимок и сверьте координаты.");

            var face = faces.GetByIndex(0) as ksEntity
                ?? throw new InvalidOperationException("Грань из коллекции не приводится к ksEntity.");
            return CreateSketchOn(part, face, name);
        }, ct);

    /// <summary>
    /// Создать эскиз на грани детали, заданной индексом из list_faces, и открыть на редактирование.
    /// Индекс стабилен, пока геометрия не меняется. Возвращает id эскиза.
    /// </summary>
    public Task<int> OpenSketchOnFaceIndexAsync(int faceIndex, string? name = null, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            var part = GetTopPart();
            var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection
                ?? throw new InvalidOperationException("Не удалось получить коллекцию граней.");
            if (faceIndex < 0 || faceIndex >= faces.GetCount())
                throw new ArgumentOutOfRangeException(nameof(faceIndex),
                    $"Индекс грани вне диапазона [0; {faces.GetCount() - 1}]. Сверьтесь с list_faces.");

            var face = faces.GetByIndex(faceIndex) as ksEntity
                ?? throw new InvalidOperationException("Грань по индексу не приводится к ksEntity.");
            return CreateSketchOn(part, face, name);
        }, ct);

    /// <summary>
    /// Создать смещённую плоскость (база + offset мм) и открыть на ней эскиз. Возвращает id эскиза.
    /// Снимает ограничение «только базовые плоскости»: эскиз на произвольной высоте/смещении.
    /// </summary>
    public Task<int> OpenSketchOnOffsetPlaneAsync(BasePlane basePlane, double offset, bool direction = true, string? name = null, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
        {
            if (!double.IsFinite(offset))
                throw new ArgumentException("Смещение должно быть конечным числом.", nameof(offset));
            var part = GetTopPart();
            var planeEntity = NewEntity(part, Obj3dType.o3d_planeOffset, null);
            var pdef = planeEntity.GetDefinition() as ksPlaneOffsetDefinition
                ?? throw new InvalidOperationException("GetDefinition() плоскости вернул не ksPlaneOffsetDefinition.");
            var baseEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(basePlane)) as ksEntity
                ?? throw new InvalidOperationException("Не удалось получить базовую плоскость.");
            pdef.SetPlane(baseEntity);
            pdef.offset = offset;
            pdef.direction = direction;
            if (!planeEntity.Create())
                throw new InvalidOperationException("Create() смещённой плоскости вернул FALSE.");
            return CreateSketchOn(part, planeEntity, name);
        }, ct);

    /// <summary>Общее ядро: создать эскиз на заданной плоскости/грани и открыть на редактирование.</summary>
    private int CreateSketchOn(ksPart part, ksEntity planeEntity, string? name)
    {
        var entity = NewEntity(part, Obj3dType.o3d_sketch, name);
        var def = entity.GetDefinition() as ksSketchDefinition
            ?? throw new InvalidOperationException("GetDefinition() эскиза вернул не ksSketchDefinition.");

        def.SetPlane(planeEntity);
        if (!entity.Create())
            throw new InvalidOperationException("Create() эскиза вернул FALSE.");

        var editor = def.BeginEdit() as ksDocument2D
            ?? throw new InvalidOperationException("BeginEdit() не вернул ksDocument2D.");

        var id = _nextId++;
        _sketches[id] = new SketchEntry { Entity = entity, Definition = def, Editor = editor };
        return id;
    }

    /// <summary>
    /// Добавить пакет примитивов в открытый эскиз за ОДИН заход на STA-поток. Целый контур
    /// (десяток отрезков и окружностей) обходится одним COM-сеансом вместо вызова на примитив.
    /// <paramref name="autoClose"/> — завершить редактирование эскиза сразу после построения.
    /// Возвращает по элементу на примитив: заметку о построенном (пустая строка — сказать нечего,
    /// у надписи это её фактическая длина, которую иначе не узнать до выдавливания).
    /// </summary>
    public Task<IReadOnlyList<string>> AddEntitiesAsync(int sketchId, IReadOnlyList<SketchEntity> entities, bool autoClose = false, CancellationToken ct = default)
        => _dispatcher.InvokeAsync<IReadOnlyList<string>>(() =>
        {
            ArgumentNullException.ThrowIfNull(entities);
            var sketch = RequireOpenSketch(sketchId);
            var editor = sketch.Editor!;
            var notes = new string[entities.Count];

            for (int i = 0; i < entities.Count; i++)
            {
                var e = entities[i] ?? throw new ArgumentException($"Примитив [{i}] пустой.", nameof(entities));
                try
                {
                    notes[i] = AddEntityCore(editor, e);
                }
                catch (Exception ex)
                {
                    // Позиция в пакете — иначе по тексту ошибки не понять, какой из примитивов не построился.
                    throw new InvalidOperationException(
                        $"Примитив [{i}] ({e.Kind.ToString().ToLowerInvariant()}) не построен: {ex.Message}", ex);
                }
            }

            if (autoClose) CloseSketchCore(sketch);
            return notes;
        }, ct);

    private string AddEntityCore(ksDocument2D editor, SketchEntity e)
    {
        switch (e.Kind)
        {
            case SketchEntityKind.Line:
                LineCore(editor, Point(e, 0), Point(e, 1), MainLineStyle);
                break;
            case SketchEntityKind.Axis:
                LineCore(editor, Point(e, 0), Point(e, 1), AxisLineStyle);
                break;
            case SketchEntityKind.Rectangle:
                RectangleCore(editor, Point(e, 0), Point(e, 1));
                break;
            case SketchEntityKind.Circle:
                CircleCore(editor, e.CenterX, e.CenterY, e.Radius);
                break;
            case SketchEntityKind.Arc:
                ArcByAngleCore(editor, e.CenterX, e.CenterY, e.Radius, e.StartAngle, e.EndAngle, e.CounterClockwise);
                break;
            case SketchEntityKind.Arc3Points:
                Arc3PointsCore(editor, Point(e, 0), Point(e, 1), Point(e, 2));
                break;
            case SketchEntityKind.Ellipse:
                EllipseCore(editor, e.CenterX, e.CenterY, e.SemiMajor, e.SemiMinor, e.Angle);
                break;
            case SketchEntityKind.Polyline:
                PolylineCore(editor, e.Points, e.Closed);
                break;
            case SketchEntityKind.Polygon:
                PolygonCore(editor, e.CenterX, e.CenterY, e.VertexCount, e.Radius, e.Inscribed, e.Angle);
                break;
            case SketchEntityKind.Spline:
                SplineCore(editor, e.Points, e.Closed);
                break;
            case SketchEntityKind.Point:
                PointCore(editor, Point(e, 0));
                break;
            case SketchEntityKind.Text:
                return TextCore(editor, Point(e, 0), e).Describe(e.Text);
            default:
                throw new ArgumentOutOfRangeException(nameof(e), $"Неизвестный тип примитива: {e.Kind}.");
        }
        return "";
    }

    /// <summary>Точка пакета по позиции — с внятной ошибкой вместо IndexOutOfRange.</summary>
    private static (double x, double y) Point(SketchEntity e, int index)
    {
        if (e.Points.Count <= index)
            throw new ArgumentException(
                $"Для типа {e.Kind.ToString().ToLowerInvariant()} нужно минимум {index + 1} точек, передано {e.Points.Count}.");
        return e.Points[index];
    }

    /// <summary>Добавить отрезок в открытый эскиз.</summary>
    public Task AddLineAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
            LineCore(RequireOpenSketch(sketchId).Editor!, (x1, y1), (x2, y2), MainLineStyle), ct);

    /// <summary>Добавить окружность в открытый эскиз.</summary>
    public Task AddCircleAsync(int sketchId, double centerX, double centerY, double radius, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
            CircleCore(RequireOpenSketch(sketchId).Editor!, centerX, centerY, radius), ct);

    /// <summary>Добавить прямоугольник (4 отрезка) по двум углам.</summary>
    public Task AddRectangleAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
            RectangleCore(RequireOpenSketch(sketchId).Editor!, (x1, y1), (x2, y2)), ct);

    /// <summary>Добавить осевую линию (стиль «осевая») в эскиз — ось для операции вращения.</summary>
    public Task AddAxisAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
            LineCore(RequireOpenSketch(sketchId).Editor!, (x1, y1), (x2, y2), AxisLineStyle), ct);

    /// <summary>Завершить редактирование эскиза.</summary>
    public Task CloseSketchAsync(int sketchId, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => CloseSketchCore(RequireSketch(sketchId)), ct);

    // ---- Новые примитивы (пакет A) -----------------------------------------

    private const short SplineOrder = 4; // порядок NURBS (степень+1); 4 = кубический сплайн
    private const int PointStyle = 0;    // системный стиль точки (см. SDK pstyles)

    /// <summary>Добавить дугу по трём точкам (начало, точка на дуге, конец) в открытый эскиз.</summary>
    public Task AddArc3PointsAsync(int sketchId, double x1, double y1, double x2, double y2, double x3, double y3, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
            Arc3PointsCore(RequireOpenSketch(sketchId).Editor!, (x1, y1), (x2, y2), (x3, y3)), ct);

    /// <summary>Добавить дугу по центру, радиусу и углам (градусы) в открытый эскиз.</summary>
    public Task AddArcByAngleAsync(int sketchId, double centerX, double centerY, double radius, double startAngle, double endAngle, bool counterClockwise = true, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
            ArcByAngleCore(RequireOpenSketch(sketchId).Editor!, centerX, centerY, radius, startAngle, endAngle, counterClockwise), ct);

    /// <summary>Добавить эллипс: центр, полуоси A/B (мм), угол наклона большой оси к X (градусы).</summary>
    public Task AddEllipseAsync(int sketchId, double centerX, double centerY, double semiMajor, double semiMinor, double angle = 0, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
            EllipseCore(RequireOpenSketch(sketchId).Editor!, centerX, centerY, semiMajor, semiMinor, angle), ct);

    /// <summary>Добавить ломаную (цепочку отрезков) по списку точек. closed замыкает последнюю с первой.</summary>
    public Task AddPolylineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
            PolylineCore(RequireOpenSketch(sketchId).Editor!, points, closed), ct);

    /// <summary>Добавить правильный многоугольник: центр, vertexCount вершин, radius (мм),
    /// inscribed=true — вершины на окружности (вписанный), false — стороны касаются (описанный),
    /// angle — поворот первой вершины (градусы).</summary>
    public Task AddPolygonAsync(int sketchId, double centerX, double centerY, int vertexCount, double radius, bool inscribed = true, double angle = 0, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
            PolygonCore(RequireOpenSketch(sketchId).Editor!, centerX, centerY, vertexCount, radius, inscribed, angle), ct);

    /// <summary>Добавить сплайн (кубический NURBS, порядок 4) через список точек. closed замыкает кривую.</summary>
    public Task AddSplineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() =>
            SplineCore(RequireOpenSketch(sketchId).Editor!, points, closed), ct);

    /// <summary>Добавить точку в открытый эскиз (опорная точка для построений).</summary>
    public Task AddPointAsync(int sketchId, double x, double y, CancellationToken ct = default)
        => _dispatcher.InvokeAsync(() => PointCore(RequireOpenSketch(sketchId).Editor!, (x, y)), ct);

    // ---- Синхронные ядра примитивов ----------------------------------------
    // Общие для одиночного вызова и для пакета: пакет прогоняет их в одном заходе на STA-поток.

    private static void LineCore(ksDocument2D editor, (double x, double y) a, (double x, double y) b, int style)
    {
        if (editor.ksLineSeg(a.x, a.y, b.x, b.y, style) == 0)
            throw new InvalidOperationException("ksLineSeg вернул 0 (отрезок не создан).");
    }

    private static void CircleCore(ksDocument2D editor, double centerX, double centerY, double radius)
    {
        if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "Радиус должен быть > 0.");
        if (editor.ksCircle(centerX, centerY, radius, MainLineStyle) == 0)
            throw new InvalidOperationException("ksCircle вернул 0 (окружность не создана).");
    }

    private static void RectangleCore(ksDocument2D editor, (double x, double y) a, (double x, double y) b)
    {
        LineCore(editor, (a.x, a.y), (b.x, a.y), MainLineStyle);
        LineCore(editor, (b.x, a.y), (b.x, b.y), MainLineStyle);
        LineCore(editor, (b.x, b.y), (a.x, b.y), MainLineStyle);
        LineCore(editor, (a.x, b.y), (a.x, a.y), MainLineStyle);
    }

    private static void Arc3PointsCore(ksDocument2D editor, (double x, double y) a, (double x, double y) b, (double x, double y) c)
    {
        if (editor.ksArcBy3Points(a.x, a.y, b.x, b.y, c.x, c.y, MainLineStyle) == 0)
            throw new InvalidOperationException("ksArcBy3Points вернул 0 (дуга не создана — три точки коллинеарны?).");
    }

    private static void ArcByAngleCore(ksDocument2D editor, double centerX, double centerY, double radius, double startAngle, double endAngle, bool counterClockwise)
    {
        SketchGeometry.RequirePositive(radius, nameof(radius));
        var dir = SketchGeometry.ArcDirection(counterClockwise);
        if (editor.ksArcByAngle(centerX, centerY, radius, startAngle, endAngle, dir, MainLineStyle) == 0)
            throw new InvalidOperationException("ksArcByAngle вернул 0 (дуга не создана).");
    }

    private void EllipseCore(ksDocument2D editor, double centerX, double centerY, double semiMajor, double semiMinor, double angle)
    {
        SketchGeometry.RequirePositive(semiMajor, nameof(semiMajor));
        SketchGeometry.RequirePositive(semiMinor, nameof(semiMinor));
        ksEllipseParam? p = null;
        try
        {
            p = NewParam<ksEllipseParam>(StructType2DEnum.ko_EllipseParam);
            // ВНИМАНИЕ: interop генерирует свойства A/B с ЗАГЛАВНОЙ (не a/b как в SDK-доках). Регистр в C# важен.
            p.xc = centerX; p.yc = centerY;
            p.A = semiMajor; p.B = semiMinor;
            p.angle = angle; p.style = MainLineStyle;
            if (editor.ksEllipse(p) == 0)
                throw new InvalidOperationException("ksEllipse вернул 0 (эллипс не создан).");
        }
        finally { ReleaseCom(p); }
    }

    private static void PolylineCore(ksDocument2D editor, IReadOnlyList<(double x, double y)> points, bool closed)
    {
        // closed-ломаная из 2 точек не образует площадь — требуем минимум 3.
        SketchGeometry.RequirePoints(points, closed ? 3 : 2, nameof(points));
        for (int i = 0; i + 1 < points.Count; i++)
            LineCore(editor, points[i], points[i + 1], MainLineStyle);
        if (closed)
            LineCore(editor, points[^1], points[0], MainLineStyle);
    }

    private void PolygonCore(ksDocument2D editor, double centerX, double centerY, int vertexCount, double radius, bool inscribed, double angle)
    {
        SketchGeometry.RequireVertexCount(vertexCount);
        SketchGeometry.RequirePositive(radius, nameof(radius));
        ksRegularPolygonParam? p = null;
        try
        {
            p = NewParam<ksRegularPolygonParam>(StructType2DEnum.ko_RegularPolygonParam);
            p.count = vertexCount; p.xc = centerX; p.yc = centerY;
            p.radius = radius; p.ang = angle;
            p.describe = SketchGeometry.PolygonDescribe(inscribed);
            p.style = MainLineStyle;
            if (editor.ksRegularPolygon(p, 0) == 0)
                throw new InvalidOperationException("ksRegularPolygon вернул 0 (многоугольник не создан).");
        }
        finally { ReleaseCom(p); }
    }

    private void SplineCore(ksDocument2D editor, IReadOnlyList<(double x, double y)> points, bool closed)
    {
        SketchGeometry.RequirePoints(points, 2, nameof(points));
        if (editor.ksNurbs(SplineOrder, closed, MainLineStyle) == 0)
            throw new InvalidOperationException("ksNurbs вернул 0 (сплайн не открыт).");
        // ksNurbs открыл составной объект — ksEndObj ОБЯЗАТЕЛЕН в любом исходе, иначе редактор «застрянет».
        var completed = false;
        try
        {
            foreach (var (x, y) in points)
            {
                ksNurbsPointParam? np = null;
                try
                {
                    np = NewParam<ksNurbsPointParam>(StructType2DEnum.ko_NurbsPointParam);
                    np.x = x; np.y = y; np.weight = 1.0;
                    if (editor.ksNurbsPoint(np) == 0)
                        throw new InvalidOperationException("ksNurbsPoint вернул 0 (узел сплайна не добавлен).");
                }
                finally { ReleaseCom(np); }
            }
            completed = true;
        }
        finally
        {
            // Закрываем составной объект всегда; ошибку завершения сообщаем только на успешном пути,
            // чтобы не подменить исходное исключение из цикла.
            var end = editor.ksEndObj();
            if (completed && end == 0)
                throw new InvalidOperationException("ksEndObj вернул 0 (сплайн не завершён).");
        }
    }

    private static void PointCore(ksDocument2D editor, (double x, double y) p)
    {
        if (editor.ksPoint(p.x, p.y, PointStyle) == 0)
            throw new InvalidOperationException("ksPoint вернул 0 (точка не создана).");
    }

    // Типы динамических массивов API5 (ldefin2d.h) и биты начертания шрифта.
    private const int TextLineArrType = 3;  // TEXT_LINE_ARR — строки надписи
    private const int TextItemArrType = 4;  // TEXT_ITEM_ARR — компоненты строки
    private const int FontItalicOn = 0x40;  // ITALIC_ON
    private const int FontBoldOn = 0x100;   // BOLD_ON

    /// <summary>Габарит объекта эскиза по его reference или <c>null</c>, если КОМПАС его не отдал.
    /// Отсутствие габарита не повод рушить построение — примитив уже создан.</summary>
    private (double MinX, double MinY, double MaxX, double MaxY)? GabaritOf(ksDocument2D editor, int reference)
    {
        ksRectParam? rect = null;
        try
        {
            rect = NewParam<ksRectParam>(StructType2DEnum.ko_RectParam);
            if (editor.ksGetObjGabaritRect(reference, rect) == 0) return null;
            if (rect.GetpBot() is not ksMathPointParam bot) return null;
            if (rect.GetpTop() is not ksMathPointParam top) return null;
            try { return (bot.x, bot.y, top.x, top.y); }
            finally { ReleaseCom(bot); ReleaseCom(top); }
        }
        catch (System.Runtime.InteropServices.COMException) { return null; }
        finally { ReleaseCom(rect); }
    }

    /// <summary>
    /// Надпись в эскизе. Текст выбранным шрифтом сразу переводится в кривые
    /// (<c>ksConvertTextToCurve</c>): пока он остаётся текстом, это оформление, и формообразующая
    /// операция его не видит. Возвращает метрики надписи — по ним агент выравнивает её, иначе
    /// фактическое место, занятое буквами, известно только после выдавливания.
    /// </summary>
    private TextMetrics TextCore(ksDocument2D editor, (double x, double y) at, SketchEntity e)
    {
        var text = e.Text ?? "";
        if (text.Length == 0)
            throw new ArgumentException("Пустая надпись: задайте text.");
        SketchGeometry.RequirePositive(e.Height, "height");
        SketchGeometry.RequirePositive(e.WidthFactor, "widthFactor");

        ksTextParam? param = null;
        ksTextItemParam? item = null;
        ksTextLineParam? line = null;
        ksDynamicArray? items = null;
        ksDynamicArray? lines = null;
        try
        {
            param = NewParam<ksTextParam>(StructType2DEnum.ko_TextParam);
            param.Init();
            if (param.GetParagraphParam() is not ksParagraphParam paragraph)
                throw new InvalidOperationException("GetParagraphParam() вернул не ksParagraphParam.");
            paragraph.Init();
            paragraph.x = at.x;
            paragraph.y = at.y;
            paragraph.ang = e.Angle;
            paragraph.height = e.Height;
            paragraph.width = 0;
            param.SetParagraphParam(paragraph);

            item = NewParam<ksTextItemParam>(StructType2DEnum.ko_TextItemParam);
            item.Init();
            item.type = (int)ksTextItemEnum.ksTItString;
            item.s = text;
            if (item.GetItemFont() is not ksTextItemFont font)
                throw new InvalidOperationException("GetItemFont() вернул не ksTextItemFont.");
            font.Init();
            if (!string.IsNullOrWhiteSpace(e.FontName)) font.fontName = e.FontName;
            font.height = e.Height;
            font.ksu = e.WidthFactor;
            font.bitVector = (e.Bold ? FontBoldOn : 0) | (e.Italic ? FontItalicOn : 0);
            item.SetItemFont(font);

            items = _session.Kompas.GetDynamicArray(TextItemArrType) as ksDynamicArray
                ?? throw new InvalidOperationException("GetDynamicArray(TEXT_ITEM_ARR) вернул null.");
            items.ksAddArrayItem(-1, item);

            line = NewParam<ksTextLineParam>(StructType2DEnum.ko_TextLineParam);
            line.Init();
            line.style = MainLineStyle;
            line.SetTextItemArr(items);

            lines = _session.Kompas.GetDynamicArray(TextLineArrType) as ksDynamicArray
                ?? throw new InvalidOperationException("GetDynamicArray(TEXT_LINE_ARR) вернул null.");
            lines.ksAddArrayItem(-1, line);
            param.SetTextLineArr(lines);

            var reference = editor.ksTextEx(param, 0);
            if (reference == 0)
                throw new InvalidOperationException(
                    $"ksTextEx вернул 0 (надпись не создана — шрифт «{e.FontName}» установлен в системе?).");

            var advance = editor.ksGetTextLengthFromReference(reference);
            var curves = editor.ksConvertTextToCurve(reference);
            if (curves == 0)
                throw new InvalidOperationException("ksConvertTextToCurve вернул 0 (надпись не переведена в кривые).");
            // Габарит берём по ссылке на КРИВЫЕ, которую вернула конвертация: у исходного текстового
            // объекта рамка идёт по ячейке строки, а она шире глифов (на скриптовых шрифтах — до 8 %).
            return new TextMetrics(advance, GabaritOf(editor, curves));
        }
        finally
        {
            // Динамические массивы живут в КОМПАС, а не в CLR: без ksDeleteArray они останутся в памяти.
            lines?.ksDeleteArray();
            items?.ksDeleteArray();
            ReleaseCom(lines);
            ReleaseCom(items);
            ReleaseCom(line);
            ReleaseCom(item);
            ReleaseCom(param);
        }
    }
}
