Итерация 2: плоскость уходит с эскизом, list_edges одним вызовом
Протокол второго прогона (48 вызовов, фаска по всему контуру надписи одной операцией): feature_delete эскиза уносит и смещённую плоскость, созданную ради него, — её id наружу не выдавался, и в дереве оставалась «Смещенная плоскость:N», которую нечем убрать. list_edges отдаёт разбивку по типам и готовую строку индексов; при >60 рёбрах печатает только их. Раньше субагент звал инструмент шесть раз, перебирая type, чтобы убедиться, что не пропущен тип кривой. Навык: исправлено МОЁ ЖЕ утверждение из прошлой итерации — карман subtract опасен для глифов (высота 3 мм), а не для каймы (2 мм, в слой кармана не попадает); запрет на пакетную обработку рёбер текста сужен до скруглений верхних рёбер — фаска по 59 рёбрам нижнего контура строится с первой попытки; добавлены формула кегля через одну пробу и кайма для прямоугольной плашки вторым примитивом.
This commit is contained in:
@@ -68,6 +68,9 @@ a *general* tool with tests, and describe the method in the `kompas-3d` skill.
|
||||
- **Re-editing an operation**: `feature_delete` now removes a sketch or feature (dependents go with it),
|
||||
but there is still no `sketch_update` — changing one dimension means deleting the sketch and rebuilding
|
||||
everything downstream. Re-entering `ksSketchDefinition.BeginEdit` on an existing sketch is unexplored.
|
||||
- **No minimum-clearance query**: the second regression run caught a 0.695 mm channel between a glyph
|
||||
and a frame wall (below 2·w for FDM) only by doing arithmetic on the sketch metrics — `describe_model`
|
||||
knows nothing about it. A general «minimum distance between these faces/bodies» would close it.
|
||||
- **`model_snapshot` takes no view** — raised by both regression runs: the image comes from whatever
|
||||
orientation the window happens to have, so a flat part cannot be checked from the front. Look at
|
||||
the projection API (`ksDocument3D`/`IViewProjectionCollection`) before adding a `view` parameter.
|
||||
|
||||
@@ -161,7 +161,12 @@ OldMan», «Карман под площадку», «Скругления уг
|
||||
`t / sin(θ/2)`, а не на `t` (Lobster: слева +0.90 при t=0.9, справа +1.70 — терминал «n»).
|
||||
Величина принадлежит не шрифту, а КОНКРЕТНОМУ начертанию: растянув тот же Lobster
|
||||
(`widthFactor` 1.29), терминал заострился и правый прирост стал +1.99. Поэтому переносить число
|
||||
между вариантами нельзя — **перезамеряй габарит после каждой смены `height`/`widthFactor`**.
|
||||
между вариантами нельзя — **перезамеряй габарит после смены `widthFactor`**.
|
||||
- **Кегль под заданный габарит считается за ОДНУ пробу, а не подбирается.** При фиксированном
|
||||
`widthFactor` прирост каймы Δ (сумма по обеим сторонам) от кегля не зависит — фигура подобна.
|
||||
Значит: построил пробу с любым `height₀`, замерил ширину глифов `W₀` и суммарный прирост Δ,
|
||||
дальше `height = height₀ · (W_цель − Δ) / W₀` — и целевой габарит получается с первого раза.
|
||||
Замер: проба h=17.06 дала Δ=4.62 (слева 1.50, справа 3.12 — миттер ×2.08) → h=17.00 → 89.98 мм.
|
||||
- **Разрядки (трекинга) у примитива нет — её набирают пробелами.** Ширину пробела для конкретного
|
||||
шрифта и кегля вычисляют двумя пробами: «EDITION» → 38.38 мм, «E D I T I O N» → 47.11 мм,
|
||||
значит пробел 1.455 мм (Bevan, cap 5.5).
|
||||
@@ -171,23 +176,32 @@ OldMan», «Карман под площадку», «Скругления уг
|
||||
каймой. `inward` съедает контур внутрь (рамка по периметру плашки), `both` делает контур средней
|
||||
линией стенки. **Проверяй стенку по объёму:** если толщина не применилась, КОМПАС молча строит
|
||||
сплошное сечение — габарит при этом тот же, и по нему подмену не увидеть.
|
||||
- **Кайма вокруг ПРЯМОУГОЛЬНОЙ плашки — вторым примитивом, а не тонкой стенкой.** Стенка обходит
|
||||
контуры эскиза, а плашка по playbook'у строится `primitive`, и каймы у неё не появится. Клади под
|
||||
неё второй блок на `2·t` больше и на высоту подложки (85 × 13.5 × 2 под плашкой 82 × 10.5 × 3) —
|
||||
получается тот же кант. Сводить прямоугольник в один эскиз с буквами не надо: контуры пересекутся,
|
||||
и эскиз станет невалидным.
|
||||
- **Рельеф = разница глубин.** Основание 2 мм + буквы 3 мм от той же плоскости: буквы выступают
|
||||
на 1 мм. Не строй буквы «на грани основания» — из одного эскиза на базовой плоскости получается
|
||||
и то и другое, и рельеф не зависит от порядка операций.
|
||||
- **Проверяй объёмом, а не габаритом.** Буквы дают сотни NURBS-рёбер, и ни снимок, ни габарит не
|
||||
покажут, что операция построила не то: сравнивай ПРИРОСТ объёма (`describe_model(sections="mass")`)
|
||||
с прикидкой «площадь контура × высота». Именно так ловится молча не применившаяся тонкая стенка.
|
||||
- **Скруглять рельефный текст можно, но по одному-двум рёбрам, а не пакетом.** Проверено на
|
||||
Zilla Slab «AB» (203 ребра): одиночное ребро скругляется R0.1 и R0.2, а `fillet_edge` сразу по
|
||||
всем рёбрам отказывает при обоих радиусах (соседние скругления конфликтуют). Адресно выбрать
|
||||
«только верхние» рёбра пока нечем — `list_edges` не отдаёт ни координат, ни принадлежности грани.
|
||||
- **Скруглять ВЕРХНИЕ рёбра рельефа пакетом нельзя, а снимать фаску по нижнему контуру — можно.**
|
||||
Проверено на Zilla Slab «AB» (203 ребра): одиночное верхнее ребро скругляется R0.1 и R0.2, а
|
||||
`fillet_edge` сразу по всем отказывает при обоих радиусах — соседние скругления конфликтуют.
|
||||
Зато `chamfer_edge(0.3, edgeIndices=[…])` по **59 рёбрам** нижнего силуэта надписи (24 прямых,
|
||||
32 NURBS, 3 дуги, включая огрызки 0.03 мм) строится с первой попытки: фаска против «слоновьей
|
||||
ноги» по всему контуру — это ОДНА операция. Рёбра нижнего контура берутся окном:
|
||||
`list_edges(minZ=-0.01, maxZ=0.01)`.
|
||||
- **`primitive(subtract)` режет ВСЁ, что попало в его габарит — включая уже построенные буквы.**
|
||||
В playbook'е шильдика карман вычитается после надписи, и если низ каймы букв опускается ниже
|
||||
верха кармана, карман срежет буквы, а построение останется «чистым». Перед вычитанием сверь
|
||||
числа: нижняя граница каймы (из метрик надписи + прирост каймы) должна быть выше верхней
|
||||
границы кармана. Проверка постфактум — **точное равенство убыли объёма** расчётному
|
||||
(79.5 × 10.5 × 1.0 = 834.75 против фактических 834.8 мм³): любое касание соседней геометрии
|
||||
сразу ломает это равенство, а габарит и снимок его не покажут.
|
||||
Сверять надо **глифы, а не кайму**, и только те, что дотягиваются до кармана ПО ВЫСОТЕ: карман
|
||||
обычно вычитается от z основания вверх (например от 2 до 3), а кайма ростом всего 2 мм в этот
|
||||
слой не попадает — она может сколько угодно перекрывать плашку в плане, и именно это перекрытие
|
||||
держит деталь одним телом. Опасны буквы: они идут на всю высоту 3 мм, поэтому их нижняя кромка
|
||||
по Y должна быть выше верхней кромки кармана. Проверка постфактум — **точное равенство убыли
|
||||
объёма** расчётному (79 × 7.5 × 1.0 = 592.5 против фактических 592.5 мм³): любое касание соседней
|
||||
геометрии сразу ломает равенство, а габарит и снимок его не покажут.
|
||||
- **Углы вокруг надписи — одним `fillet_edge(points=[…])`.** На теле с сотнями рёбер список из
|
||||
`list_edges` неподъёмен, но точки углов известны из построения: передай их списком, и все рёбра
|
||||
уйдут в ОДНУ операцию дерева. Если ребра в части точек нет (типичный случай — угол «съеден»
|
||||
|
||||
@@ -599,16 +599,25 @@ public sealed partial class PartModeler
|
||||
$"{what} id={id} не удалена: DeleteObject вернул FALSE. " +
|
||||
"Объект уже удалён или занят другой операцией?");
|
||||
|
||||
var alsoPlane = false;
|
||||
if (isSketch)
|
||||
{
|
||||
ReleaseCom(sketch!.Definition);
|
||||
// Смещённая плоскость создавалась ради этого эскиза: без неё в дереве остаётся
|
||||
// «Смещённая плоскость:N», удалить которую агенту нечем — её id наружу не выдаётся.
|
||||
if (sketch!.OwnedPlane is { } plane)
|
||||
{
|
||||
alsoPlane = doc3d.DeleteObject(plane);
|
||||
ReleaseCom(plane);
|
||||
}
|
||||
ReleaseCom(sketch.Definition);
|
||||
_sketches.Remove(id);
|
||||
}
|
||||
else _features.Remove(id);
|
||||
|
||||
ReleaseCom(entity);
|
||||
doc3d.RebuildDocument();
|
||||
return name.Length == 0 ? $"{what} id={id}" : $"{what} id={id} «{name}»";
|
||||
var title = name.Length == 0 ? $"{what} id={id}" : $"{what} id={id} «{name}»";
|
||||
return alsoPlane ? $"{title} (вместе со смещённой плоскостью)" : title;
|
||||
}, ct);
|
||||
|
||||
/// <summary>Имя объекта для сообщения; у уже нежизнеспособного COM-объекта его может не быть.</summary>
|
||||
|
||||
@@ -80,11 +80,12 @@ public sealed partial class PartModeler
|
||||
pdef.direction = direction;
|
||||
if (!planeEntity.Create())
|
||||
throw new InvalidOperationException("Create() смещённой плоскости вернул FALSE.");
|
||||
return CreateSketchOn(part, planeEntity, name);
|
||||
// Плоскость создана ради этого эскиза — она уйдёт из дерева вместе с ним.
|
||||
return CreateSketchOn(part, planeEntity, name, ownedPlane: planeEntity);
|
||||
}, ct);
|
||||
|
||||
/// <summary>Общее ядро: создать эскиз на заданной плоскости/грани и открыть на редактирование.</summary>
|
||||
private int CreateSketchOn(ksPart part, ksEntity planeEntity, string? name)
|
||||
private int CreateSketchOn(ksPart part, ksEntity planeEntity, string? name, ksEntity? ownedPlane = null)
|
||||
{
|
||||
var entity = NewEntity(part, Obj3dType.o3d_sketch, name);
|
||||
var def = entity.GetDefinition() as ksSketchDefinition
|
||||
@@ -98,7 +99,7 @@ public sealed partial class PartModeler
|
||||
?? throw new InvalidOperationException("BeginEdit() не вернул ksDocument2D.");
|
||||
|
||||
var id = _nextId++;
|
||||
_sketches[id] = new SketchEntry { Entity = entity, Definition = def, Editor = editor };
|
||||
_sketches[id] = new SketchEntry { Entity = entity, Definition = def, Editor = editor, OwnedPlane = ownedPlane };
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Runtime.Versioning;
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Interop;
|
||||
using Kompas.Mcp.Core.Threading;
|
||||
using Kompas6API5;
|
||||
@@ -38,6 +38,11 @@ public sealed partial class PartModeler : IDisposable
|
||||
public required ksEntity Entity { get; init; }
|
||||
public required ksSketchDefinition Definition { get; init; }
|
||||
public ksDocument2D? Editor { get; set; } // не null, пока эскиз открыт на редактирование
|
||||
|
||||
/// <summary>Смещённая плоскость, созданная РАДИ этого эскиза (у эскизов на базовой
|
||||
/// плоскости или грани — null). Удаляется вместе с ним: иначе в дереве остаётся
|
||||
/// «Смещённая плоскость:N», которую агенту нечем убрать — её id наружу не выдаётся.</summary>
|
||||
public ksEntity? OwnedPlane { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -157,6 +162,7 @@ public sealed partial class PartModeler : IDisposable
|
||||
ReleaseCom(s.Editor);
|
||||
ReleaseCom(s.Definition);
|
||||
ReleaseCom(s.Entity);
|
||||
ReleaseCom(s.OwnedPlane);
|
||||
}
|
||||
foreach (var f in _features.Values)
|
||||
ReleaseCom(f);
|
||||
|
||||
@@ -94,19 +94,27 @@ public sealed class QueryTools(KompasSession session, QueryService query, ModelI
|
||||
? "Рёбер нет (модель пуста?)."
|
||||
: "Под отбор не попало ни одно ребро — ослабьте окно или уберите type.";
|
||||
|
||||
// Координаты печатаем, когда список обозрим: на сотнях рёбер они топят ответ, а пользы
|
||||
// без отбора всё равно нет — сначала сузьте окно.
|
||||
var withPoints = edges.Count <= 60;
|
||||
// Разбивка по типам — чтобы не перебирать type вручную вызов за вызовом.
|
||||
var byType = string.Join(", ", edges.GroupBy(e => e.Type)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.Select(g => $"{g.Key} {g.Count()}"));
|
||||
var head = (filter.IsEmpty ? $"Рёбер: {edges.Count}" : $"Рёбер под отбор: {edges.Count}") + $" ({byType})";
|
||||
|
||||
// Готовая строка индексов: она и есть то, что уходит в fillet_edge/chamfer_edge.
|
||||
var indices = "индексы: " + string.Join(",", edges.Select(e => e.Index));
|
||||
|
||||
// Построчно печатаем, только пока список обозрим: на сотнях рёбер это топит ответ,
|
||||
// а для пакетной операции достаточно индексов.
|
||||
if (edges.Count > 60)
|
||||
return $"{head}\n{indices}\n(подробности скрыты — сузьте отбор, чтобы увидеть длины и концы)";
|
||||
|
||||
var lines = edges.Select(e =>
|
||||
{
|
||||
var head = Inv($" [{e.Index}] {e.Type}, длина {e.Length:F2} мм");
|
||||
if (!withPoints || e.Start is not { } s || e.End is not { } en) return head;
|
||||
return head + Inv($", концы ({s.X:F2}, {s.Y:F2}, {s.Z:F2}) → ({en.X:F2}, {en.Y:F2}, {en.Z:F2})");
|
||||
var row = Inv($" [{e.Index}] {e.Type}, длина {e.Length:F2} мм");
|
||||
if (e.Start is not { } s || e.End is not { } en) return row;
|
||||
return row + Inv($", концы ({s.X:F2}, {s.Y:F2}, {s.Z:F2}) → ({en.X:F2}, {en.Y:F2}, {en.Z:F2})");
|
||||
});
|
||||
|
||||
var head = filter.IsEmpty ? $"Рёбер: {edges.Count}" : $"Рёбер под отбор: {edges.Count}";
|
||||
var hint = withPoints ? "" : "\n(координаты скрыты — сузьте отбор, чтобы их увидеть)";
|
||||
return head + "\n" + string.Join("\n", lines) + hint;
|
||||
return $"{head}\n{indices}\n" + string.Join("\n", lines);
|
||||
}
|
||||
|
||||
private static string Inv(FormattableString fs) => FormattableString.Invariant(fs);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Modeling;
|
||||
using Kompas.Mcp.Core.Query;
|
||||
|
||||
@@ -86,6 +86,39 @@ public sealed class FeatureDeleteTests : IntegrationTestBase
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Deleting_a_sketch_takes_its_offset_plane_with_it()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Тело нужно, чтобы дерево вообще перечислялось: у детали без тел оно пустое.
|
||||
var plate = await _modeler.OpenSketchAsync(BasePlane.XOY, name: "Контур плиты");
|
||||
await _modeler.AddRectangleAsync(plate, 0, 0, 20, 10);
|
||||
await _modeler.CloseSketchAsync(plate);
|
||||
await _modeler.ExtrudeAsync(plate, depth: 3, name: "Плита");
|
||||
|
||||
// Плоскость создаётся РАДИ эскиза, её id наружу не выдаётся — останься она в дереве,
|
||||
// убрать её агенту было бы нечем.
|
||||
var s = await _modeler.OpenSketchOnOffsetPlaneAsync(BasePlane.XOY, offset: 5, name: "Проба на смещении");
|
||||
await _modeler.AddRectangleAsync(s, 2, 2, 8, 8);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.RebuildAsync();
|
||||
var before = await TreeNamesAsync();
|
||||
Assert.Contains(before, n => n.Contains("мещ", StringComparison.OrdinalIgnoreCase)); // плоскость в дереве есть
|
||||
|
||||
var deleted = await _modeler.DeleteAsync(s);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
Assert.Contains("плоскост", deleted, StringComparison.OrdinalIgnoreCase);
|
||||
var names = await TreeNamesAsync();
|
||||
Assert.DoesNotContain("Проба на смещении", names);
|
||||
Assert.DoesNotContain(names, n => n.Contains("мещ", StringComparison.OrdinalIgnoreCase)); // «Смещенная плоскость:N»
|
||||
Assert.Contains("Плита", names);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unknown_id_says_what_can_be_deleted()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user