kompas-kernel 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. kompas_kernel/__init__.py +3 -0
  2. kompas_kernel/features/__init__.py +9 -0
  3. kompas_kernel/features/assembly_navigator/__init__.py +8 -0
  4. kompas_kernel/features/assembly_navigator/commands.py +37 -0
  5. kompas_kernel/features/assembly_navigator/ksapi/__init__.py +5 -0
  6. kompas_kernel/features/assembly_navigator/ksapi/mapping.py +122 -0
  7. kompas_kernel/features/assembly_navigator/ksapi/presentation.py +107 -0
  8. kompas_kernel/features/assembly_navigator/ksapi/raw.py +74 -0
  9. kompas_kernel/features/assembly_navigator/ksapi/reader.py +287 -0
  10. kompas_kernel/features/assembly_navigator/ksapi/resolver.py +35 -0
  11. kompas_kernel/features/assembly_navigator/models.py +218 -0
  12. kompas_kernel/features/assembly_navigator/queries.py +42 -0
  13. kompas_kernel/features/assembly_navigator/service.py +31 -0
  14. kompas_kernel/features/assembly_navigator/summary.py +90 -0
  15. kompas_kernel/features/document_session/__init__.py +5 -0
  16. kompas_kernel/features/document_session/checkpointed.py +165 -0
  17. kompas_kernel/features/document_session/commands.py +94 -0
  18. kompas_kernel/features/document_session/digest.py +126 -0
  19. kompas_kernel/features/document_session/ksapi/__init__.py +5 -0
  20. kompas_kernel/features/document_session/ksapi/context_reader.py +174 -0
  21. kompas_kernel/features/document_session/ksapi/screenshot.py +365 -0
  22. kompas_kernel/features/document_session/ksapi/snapshot.py +181 -0
  23. kompas_kernel/features/document_session/models.py +311 -0
  24. kompas_kernel/features/document_session/queries.py +62 -0
  25. kompas_kernel/features/document_session/registry.py +101 -0
  26. kompas_kernel/features/document_session/service.py +128 -0
  27. kompas_kernel/features/geometry_3d/__init__.py +9 -0
  28. kompas_kernel/features/geometry_3d/axis_math.py +50 -0
  29. kompas_kernel/features/geometry_3d/commands.py +68 -0
  30. kompas_kernel/features/geometry_3d/hex_detect.py +113 -0
  31. kompas_kernel/features/geometry_3d/ksapi/__init__.py +5 -0
  32. kompas_kernel/features/geometry_3d/ksapi/component_geometry.py +559 -0
  33. kompas_kernel/features/geometry_3d/ksapi/measurements.py +368 -0
  34. kompas_kernel/features/geometry_3d/ksapi/presentation.py +204 -0
  35. kompas_kernel/features/geometry_3d/models.py +216 -0
  36. kompas_kernel/features/geometry_3d/queries.py +96 -0
  37. kompas_kernel/features/geometry_3d/service.py +69 -0
  38. kompas_kernel/features/geometry_3d/wrench_zone.py +161 -0
  39. kompas_kernel/features/ksapi_automation/__init__.py +9 -0
  40. kompas_kernel/features/ksapi_automation/gate.py +282 -0
  41. kompas_kernel/features/ksapi_automation/models.py +75 -0
  42. kompas_kernel/features/ksapi_automation/namespace.py +12 -0
  43. kompas_kernel/features/ksapi_automation/run_python.py +133 -0
  44. kompas_kernel/features/ksapi_automation/runner.py +126 -0
  45. kompas_kernel/features/ksapi_automation/service.py +85 -0
  46. kompas_kernel/features/part_authoring/__init__.py +5 -0
  47. kompas_kernel/features/part_authoring/build_geometry.py +122 -0
  48. kompas_kernel/features/part_authoring/models.py +99 -0
  49. kompas_kernel/features/wrench_clearance/__init__.py +7 -0
  50. kompas_kernel/features/wrench_clearance/models.py +87 -0
  51. kompas_kernel/features/wrench_clearance/service.py +154 -0
  52. kompas_kernel/kompas/__init__.py +14 -0
  53. kompas_kernel/kompas/errors.py +11 -0
  54. kompas_kernel/kompas/models.py +68 -0
  55. kompas_kernel/kompas/objects.py +93 -0
  56. kompas_kernel/kompas/platform.py +40 -0
  57. kompas_kernel/kompas/runtime.py +186 -0
  58. kompas_kernel/kompas/session.py +195 -0
  59. kompas_kernel/kompas/units.py +48 -0
  60. kompas_kernel/ksapi_static/__init__.py +8 -0
  61. kompas_kernel/ksapi_static/facts.py +132 -0
  62. kompas_kernel/ksapi_static/inventory.py +185 -0
  63. kompas_kernel/ksapi_static/recipes.py +114 -0
  64. kompas_kernel/ksapi_static/wrapper_grep.py +83 -0
  65. kompas_kernel/observability/__init__.py +1 -0
  66. kompas_kernel/observability/logger.py +24 -0
  67. kompas_kernel/observability/setup.py +115 -0
  68. kompas_kernel/observability/taxonomy.py +27 -0
  69. kompas_kernel/py.typed +1 -0
  70. kompas_kernel/standards/__init__.py +1 -0
  71. kompas_kernel/standards/wrench_clearance.py +234 -0
  72. kompas_kernel/utils/__init__.py +1 -0
  73. kompas_kernel/utils/filesystem.py +82 -0
  74. kompas_kernel/utils/validation.py +21 -0
  75. kompas_kernel-0.0.1.dist-info/METADATA +56 -0
  76. kompas_kernel-0.0.1.dist-info/RECORD +77 -0
  77. kompas_kernel-0.0.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,216 @@
1
+ """DTO геометрии 3D — публичный контракт `queries.py`/`commands.py`/`GeometryPort`.
2
+
3
+ Перенесено из `bridge/models/geometry.py` (`DistanceMeasurement`/`NeighborHit`/
4
+ `NeighborsResult`/`AxisLine`/`FaceMesh`/`ComponentMesh`/`PlanarFaceFact`/`PlanarFaces`/
5
+ `CylindricalFaceFact`/`CylindricalFaces`/`RayHit`) и `bridge/models/presentation.py`
6
+ (`ShowDistanceResult`) — E16.04a. Framework-free: pydantic-схемы на `utils.validation.CustomModel`,
7
+ `Point3D` берётся из `kompas_kernel.kompas.models` напрямую (feature slice не импортирует
8
+ `kompas_copilot.bridge`, fitness-тест `tests/architecture/test_layer_boundaries.py`).
9
+
10
+ Универсальные CAD-примитивы: измерение расстояния, соседи, ось, меш, ray-pick, точные
11
+ B-rep грани, презентация размерных линий. Ни один не знает про ключи/ГОСТ/крепёж —
12
+ сценарная логика живёт в `eyes`.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Self
18
+
19
+ from pydantic import Field, model_validator
20
+
21
+ from kompas_kernel.kompas.models import Point3D
22
+ from kompas_kernel.utils.validation import CustomModel
23
+
24
+
25
+ class DistanceMeasurement(CustomModel):
26
+ """Минимальное расстояние между двумя компонентами сборки.
27
+
28
+ `point1`/`point2` — подтверждённые точки отрезка минимального расстояния из
29
+ `IDistanceAngleMeasurement3D.GetMinPoint1/2`. Это фактические координаты, в
30
+ отличие от `PickModelObjectsWithLimit`, где точки не используются как источник
31
+ истины для зазора.
32
+ """
33
+
34
+ component_identifier_1: str
35
+ component_identifier_2: str
36
+ distance_mm: float = Field(ge=0)
37
+ point1: Point3D
38
+ point2: Point3D
39
+ measure_result_code: int
40
+
41
+
42
+ class NeighborHit(CustomModel):
43
+ """Один сосед компонента: идентификатор + зазор + точки минимума.
44
+
45
+ `distance_mm`/`point_on_target`/`point_on_neighbor` — из транзиентного
46
+ `IMeasurement3D` (part↔part), те же фактические координаты, что у
47
+ `DistanceMeasurement` (не заглушки ray-pick). `point_on_target` лежит на исходном
48
+ компоненте (крепеже), `point_on_neighbor` — на соседе.
49
+ """
50
+
51
+ component_identifier: str
52
+ component_name: str | None
53
+ distance_mm: float = Field(ge=0)
54
+ point_on_target: Point3D
55
+ point_on_neighbor: Point3D
56
+ measure_result_code: int
57
+
58
+
59
+ class NeighborsResult(CustomModel):
60
+ """Соседи компонента в пределах `gap_mm`, по возрастанию зазора.
61
+
62
+ Механизм (детерминированный, read-only): bbox-предфильтр по `GetGabarit` отбирает
63
+ кандидатов дёшево (чистая математика), затем точный зазор до каждого считает
64
+ транзиентный `IMeasurement3D` part↔part — как `measure_min_distance`, но пачкой и с
65
+ отсевом по порогу. Универсальный примитив «что рядом с компонентом»; радиальная зона
66
+ ключа ГОСТ 13682-80 Черт. 5 — отдельный слой `eyes` поверх этого факта. `ICollision`
67
+ сюда сознательно НЕ берётся: он создаёт объект диагностики в дереве и пачкает документ
68
+ (`docs/recipes/ksapi-link.md` §«Спайк find-neighbors»), а этот путь чистый. Сам
69
+ компонент-цель в список не попадает.
70
+ """
71
+
72
+ target_identifier: str
73
+ gap_mm: float = Field(gt=0)
74
+ neighbors: list[NeighborHit]
75
+
76
+
77
+ class AxisLine(CustomModel):
78
+ """Ось компонента: точка на оси + направление, глобальная СК документа, мм.
79
+
80
+ `origin` — `IPlacement3D.GetOrigin()`: для стандартного крепежа из библиотеки
81
+ это опорная плоскость под головкой/гайкой (подтверждено спайком 01). `direction`
82
+ — `IPlacement3D.GetVector(o3d_axisOZ)`, не ручной разбор `GetMatrix3D()`
83
+ (та же причина — не гадать про row-major/column-major раскладку матрицы).
84
+ `direction` не нормируется адаптером — отдаётся ровно то, что вернул KsAPI.
85
+ """
86
+
87
+ origin: Point3D
88
+ direction: Point3D
89
+
90
+
91
+ class FaceMesh(CustomModel):
92
+ """Точки тесселляции одной грани компонента, глобальная СК документа, мм.
93
+
94
+ `face_index` — позиция грани в стабильном порядке перечисления граней
95
+ компонента (то же соглашение, что использует адаптер для индексации граней
96
+ внутри компонента); полезно для подсветки конкретной грани.
97
+ """
98
+
99
+ face_index: int = Field(ge=0)
100
+ points: list[Point3D]
101
+
102
+
103
+ class ComponentMesh(CustomModel):
104
+ """Тесселляция всех граней компонента в глобальной СК документа, мм.
105
+
106
+ Грань без тесселляции (`IFace.GetTessellation()` вернул пусто/`None`) в список
107
+ не попадает — это не ошибка, а честный пропуск непосчитанной грани.
108
+ """
109
+
110
+ faces: list[FaceMesh]
111
+
112
+
113
+ class PlanarFaceFact(CustomModel):
114
+ """Точный (не тесселлированный) факт об одной плоской грани компонента, глобальная СК, мм.
115
+
116
+ `radial_mm` — расстояние `IFace.GetDistanceToPoint()` от оси компонента (см.
117
+ `component_axis`) до плоскости этой грани, замеренное в осевой точке, где грань ещё
118
+ не обрезана (см. `component_planar_faces` докстринг) — честный перпендикуляр, не
119
+ приближение по вершинам. `corners` — угловые точки обрезанной границы грани
120
+ (`GetParamUMin/Max`×`GetParamVMin/Max` → `GetPoint`), уже в глобальной СК документа
121
+ (`IPart.TransformPoint`) — по ним вызывающий проецирует осевой диапазон грани на
122
+ ось, без обращения к тесселляции.
123
+ """
124
+
125
+ face_index: int = Field(ge=0)
126
+ radial_mm: float = Field(ge=0)
127
+ corners: list[Point3D]
128
+
129
+
130
+ class PlanarFaces(CustomModel):
131
+ """Все плоские грани компонента, чья плоскость параллельна заданной оси, глобальная СК, мм.
132
+
133
+ Грани, чья плоскость НЕ параллельна оси (торцевые/скошенные грани — расстояние до
134
+ них зависит от точки на оси, единого радиального числа не существует), в список не
135
+ попадают — это фильтр на границе адаптера, не постобработка вызывающего.
136
+ """
137
+
138
+ faces: list[PlanarFaceFact]
139
+
140
+
141
+ class CylindricalFaceFact(CustomModel):
142
+ """Точный (не тесселлированный) факт об одной цилиндрической грани, глобальная СК, мм.
143
+
144
+ `radial_mm` — расстояние от заданной оси (не обязательно оси самого компонента, см.
145
+ `component_cylindrical_faces`) до поверхности цилиндра: `abs(axis_to_axis_mm -
146
+ IFace.GetRadius())`, где `axis_to_axis_mm` — радиальный компонент точки на оси
147
+ цилиндра относительно заданной оси (та же математика `axial_and_radial_mm`, что и
148
+ для планарных фактов). Знак не нужен — расстояние от точки до окружности не зависит
149
+ от того, снаружи или внутри цилиндра лежит эта точка (`|d - R|` в обоих случаях).
150
+ `corners` — угловые точки обрезанной границы грани (те же `GetParamUMin/Max` ×
151
+ `GetParamVMin/Max` → `GetPoint`, что и у `PlanarFaceFact`), уже в глобальной СК —
152
+ по ним вызывающий проецирует осевой диапазон грани на ось.
153
+ """
154
+
155
+ face_index: int = Field(ge=0)
156
+ radial_mm: float = Field(ge=0)
157
+ corners: list[Point3D]
158
+
159
+
160
+ class CylindricalFaces(CustomModel):
161
+ """Все цилиндрические грани компонента, чья ось параллельна заданной оси, глобальная СК, мм.
162
+
163
+ Грани, чья ось НЕ параллельна заданной оси (конус под углом, отверстие поперёк —
164
+ расстояние до них не сводится к одному радиальному числу), в список не попадают —
165
+ фильтр на границе адаптера, не постобработка вызывающего.
166
+ """
167
+
168
+ faces: list[CylindricalFaceFact]
169
+
170
+
171
+ class RayHit(CustomModel):
172
+ """Результат геометрического ray-pick: только факт попадания в компонент.
173
+
174
+ Луч используется как read-only поиск соседних объектов. Координаты пересечения
175
+ намеренно не входят в DTO: спайк 03 показал заглушки на body/part-уровне, поэтому
176
+ точки луча не считаются надёжным фактом зазора.
177
+ """
178
+
179
+ hit: bool
180
+ component_identifier: str | None = None
181
+ component_name: str | None = None
182
+ model_object_type: int | None = None
183
+
184
+ @model_validator(mode="after")
185
+ def _check_hit_identifier(self) -> Self:
186
+ if self.hit and self.component_identifier is None:
187
+ raise ValueError("component_identifier is required when hit=True")
188
+ return self
189
+
190
+
191
+ class ShowDistanceResult(CustomModel):
192
+ """Результат `show_distance_dimension`: постоянная размерная линия в 3D + честный факт.
193
+
194
+ `distance_mm`/`point1`/`point2` — тот же быстрый факт, что и `measure_min_distance`
195
+ (транзиентный `IMeasurement3D`, part↔part). `shown_value_mm` — то, что реально
196
+ показывает СОЗДАННАЯ постоянная фича `IDistanceAngleMeasurement3D` после `Update()`
197
+ (`GetLmin()`); она считает только `IFace↔IFace` (спайк 02), поэтому пара граней
198
+ резолвится по точкам минимума (ближайшая тесселлированная вершина на каждом
199
+ компоненте) — хордовая погрешность мешей может дать `shown_value_mm`, заметно
200
+ отличное от `distance_mm`; расхождение — честный сигнал, не скрывается.
201
+ `zoomed` — сработал ли зум по выделению (фичи-измерения или, если её выделение не
202
+ сработало, компонента-виновника — см. докстринг `ksapi/presentation.py`). `None` в
203
+ `shown_value_mm` означает, что постоянную фичу не удалось создать/посчитать (реализация
204
+ в этом случае бросает `KompasError`, поле оставлено `Optional` только для честности
205
+ контракта DTO).
206
+
207
+ Странность, зафиксированная и оставленная как есть (не чинить, E16.04a): `zoomed=False`
208
+ схлопывает две разные причины — «ничего не выделилось, зум не пробовали» и «выделилось,
209
+ но `ExecuteKompasCommand` вернул `False`». Поля, различающего их, в DTO нет.
210
+ """
211
+
212
+ distance_mm: float = Field(ge=0)
213
+ point1: Point3D
214
+ point2: Point3D
215
+ zoomed: bool
216
+ shown_value_mm: float | None = None
@@ -0,0 +1,96 @@
1
+ """Framework-free public read API for geometry 3D, over `KompasSession`.
2
+
3
+ No Pydantic AI, no `kompas_copilot.bridge` import — the feature owns its own KsAPI
4
+ measurements/component-geometry modules (`ksapi/measurements.py`,
5
+ `ksapi/component_geometry.py`); this module only wires them through session callbacks
6
+ that receive `KompasContext` directly (no migration-only `_ACTIVE_CONTEXT` seam).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from kompas_kernel.features.geometry_3d.ksapi import component_geometry, measurements
12
+ from kompas_kernel.features.geometry_3d.models import (
13
+ AxisLine,
14
+ ComponentMesh,
15
+ CylindricalFaces,
16
+ DistanceMeasurement,
17
+ NeighborsResult,
18
+ PlanarFaces,
19
+ RayHit,
20
+ )
21
+ from kompas_kernel.kompas.models import Point3D
22
+ from kompas_kernel.kompas.session import KompasContext, KompasSession
23
+
24
+
25
+ async def measure_min_distance(
26
+ session: KompasSession,
27
+ component_identifier_1: str,
28
+ component_identifier_2: str,
29
+ ) -> DistanceMeasurement:
30
+ """Минимальное расстояние между двумя компонентами сборки (транзиентный `IMeasurement3D`)."""
31
+
32
+ def _operation(context: KompasContext) -> DistanceMeasurement:
33
+ return measurements.measure_min_distance(
34
+ context.modules, context.app, component_identifier_1, component_identifier_2
35
+ )
36
+
37
+ return await session.run(operation_name="geometry_3d.measure_min_distance", operation=_operation)
38
+
39
+
40
+ async def find_neighbors(session: KompasSession, identifier: str, gap_mm: float) -> NeighborsResult:
41
+ """Компоненты в пределах `gap_mm` от компонента `identifier`, по возрастанию зазора."""
42
+
43
+ def _operation(context: KompasContext) -> NeighborsResult:
44
+ return measurements.find_neighbors(context.modules, context.app, identifier, gap_mm)
45
+
46
+ return await session.run(operation_name="geometry_3d.find_neighbors", operation=_operation)
47
+
48
+
49
+ async def probe_ray(
50
+ session: KompasSession,
51
+ origin: Point3D,
52
+ direction: Point3D,
53
+ limit_mm: float,
54
+ ) -> RayHit:
55
+ """Найти компонент вдоль геометрической прямой (только факт попадания)."""
56
+
57
+ def _operation(context: KompasContext) -> RayHit:
58
+ return measurements.probe_ray(context.modules, context.app, origin, direction, limit_mm)
59
+
60
+ return await session.run(operation_name="geometry_3d.probe_ray", operation=_operation)
61
+
62
+
63
+ async def component_axis(session: KompasSession, identifier: str) -> AxisLine:
64
+ """Ось компонента: origin + direction, глобальная СК документа, мм."""
65
+
66
+ def _operation(context: KompasContext) -> AxisLine:
67
+ return component_geometry.component_axis(context.modules, context.app, identifier)
68
+
69
+ return await session.run(operation_name="geometry_3d.component_axis", operation=_operation)
70
+
71
+
72
+ async def component_mesh(session: KompasSession, identifier: str) -> ComponentMesh:
73
+ """Тесселляция всех граней компонента, глобальная СК документа, мм."""
74
+
75
+ def _operation(context: KompasContext) -> ComponentMesh:
76
+ return component_geometry.component_mesh(context.modules, context.app, identifier)
77
+
78
+ return await session.run(operation_name="geometry_3d.component_mesh", operation=_operation)
79
+
80
+
81
+ async def component_planar_faces(session: KompasSession, identifier: str, axis: AxisLine) -> PlanarFaces:
82
+ """Точные плоские грани компонента, чья плоскость параллельна `axis`, глобальная СК, мм."""
83
+
84
+ def _operation(context: KompasContext) -> PlanarFaces:
85
+ return component_geometry.component_planar_faces(context.modules, context.app, identifier, axis)
86
+
87
+ return await session.run(operation_name="geometry_3d.component_planar_faces", operation=_operation)
88
+
89
+
90
+ async def component_cylindrical_faces(session: KompasSession, identifier: str, axis: AxisLine) -> CylindricalFaces:
91
+ """Точные цилиндрические грани компонента, чья ось параллельна `axis`, глобальная СК, мм."""
92
+
93
+ def _operation(context: KompasContext) -> CylindricalFaces:
94
+ return component_geometry.component_cylindrical_faces(context.modules, context.app, identifier, axis)
95
+
96
+ return await session.run(operation_name="geometry_3d.component_cylindrical_faces", operation=_operation)
@@ -0,0 +1,69 @@
1
+ """`GeometryService` — thin holder of one `KompasSession` + `ShownDimensionsState`.
2
+
3
+ Exists solely because composition and its typed fake need one object implementing the
4
+ narrow consumer-owned `GeometryPort` Protocol declared in `core/capabilities/geometry_3d.py`
5
+ (same shape as `AssemblyNavigatorService`/`AssemblyNavigatorPort`) — no multi-step use case
6
+ lives here beyond delegating to `queries`/`commands`. The session is a required dependency:
7
+ this class neither creates nor closes it — the transport that built the session owns that
8
+ lifecycle. `ShownDimensionsState` (list of `IDistanceAngleMeasurement3D` references created
9
+ by `show_distance_dimension`) is owned here, one instance per service/session, replacing the
10
+ field that used to live directly on `KompasKsApiAdapter`.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from kompas_kernel.features.geometry_3d import commands, queries
16
+ from kompas_kernel.features.geometry_3d.commands import ShownDimensionsState
17
+ from kompas_kernel.features.geometry_3d.models import (
18
+ AxisLine,
19
+ ComponentMesh,
20
+ CylindricalFaces,
21
+ DistanceMeasurement,
22
+ NeighborsResult,
23
+ PlanarFaces,
24
+ RayHit,
25
+ ShowDistanceResult,
26
+ )
27
+ from kompas_kernel.kompas.models import Point3D
28
+ from kompas_kernel.kompas.session import KompasSession
29
+
30
+
31
+ class GeometryService:
32
+ """Delegates geometry queries/commands to `queries`/`commands` over one session."""
33
+
34
+ def __init__(self, session: KompasSession) -> None:
35
+ self._session = session
36
+ self._state = ShownDimensionsState()
37
+
38
+ async def measure_min_distance(
39
+ self, component_identifier_1: str, component_identifier_2: str
40
+ ) -> DistanceMeasurement:
41
+ return await queries.measure_min_distance(self._session, component_identifier_1, component_identifier_2)
42
+
43
+ async def probe_ray(self, origin: Point3D, direction: Point3D, limit_mm: float) -> RayHit:
44
+ return await queries.probe_ray(self._session, origin, direction, limit_mm)
45
+
46
+ async def find_neighbors(self, identifier: str, gap_mm: float) -> NeighborsResult:
47
+ return await queries.find_neighbors(self._session, identifier, gap_mm)
48
+
49
+ async def component_axis(self, identifier: str) -> AxisLine:
50
+ return await queries.component_axis(self._session, identifier)
51
+
52
+ async def component_mesh(self, identifier: str) -> ComponentMesh:
53
+ return await queries.component_mesh(self._session, identifier)
54
+
55
+ async def component_planar_faces(self, identifier: str, axis: AxisLine) -> PlanarFaces:
56
+ return await queries.component_planar_faces(self._session, identifier, axis)
57
+
58
+ async def component_cylindrical_faces(self, identifier: str, axis: AxisLine) -> CylindricalFaces:
59
+ return await queries.component_cylindrical_faces(self._session, identifier, axis)
60
+
61
+ async def show_distance_dimension(
62
+ self, component_identifier_1: str, component_identifier_2: str
63
+ ) -> ShowDistanceResult:
64
+ return await commands.show_distance_dimension(
65
+ self._session, component_identifier_1, component_identifier_2, self._state
66
+ )
67
+
68
+ async def clear_dimensions(self) -> int:
69
+ return await commands.clear_dimensions(self._session, self._state)
@@ -0,0 +1,161 @@
1
+ """Pure radial-clearance geometry for ГОСТ 13682-80 Черт. 5 wrench-zone checks.
2
+
3
+ Черт. 5 norms K and R (`standards/wrench_clearance.py`) are radial dimensions measured
4
+ from the fastener axis (K=E — axis to a straight edge; R — pocket wall radius; see
5
+ `docs/recipes/gost-13682-wrench-clearance.md` §2), not a surface-to-surface distance.
6
+ This module computes the honest fact to compare against them: the minimum radial
7
+ distance from a fastener's axis to a neighbor's tessellated mesh, restricted to the
8
+ axial zone where a wrench actually needs room.
9
+
10
+ No pydantic-ai, no bridge port calls: this module only consumes already-fetched
11
+ `AxisLine`/`ComponentMesh` DTOs (the caller — the geometry toolset — fetches those through
12
+ `GeometryPort` first) and returns a typed fact. stdlib/math only, so it stays fast
13
+ even on a mesh with hundreds/thousands of points (no COM in the math).
14
+
15
+ Черт. 6 (two-fastener wrench-clearance semantics: A2/E1 pairing) is out of scope here`.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Sequence
21
+
22
+ from pydantic import Field
23
+
24
+ from kompas_kernel.features.geometry_3d.axis_math import axial_and_radial_mm, unit_axis_direction
25
+ from kompas_kernel.features.geometry_3d.models import (
26
+ AxisLine,
27
+ ComponentMesh,
28
+ CylindricalFaceFact,
29
+ PlanarFaceFact,
30
+ )
31
+ from kompas_kernel.kompas.models import Point3D
32
+ from kompas_kernel.utils.validation import CustomModel
33
+
34
+
35
+ class RadialDistance(CustomModel):
36
+ """Minimum radial distance from a fastener axis to a neighbor's mesh, in an axial zone.
37
+
38
+ `point`/`face_index` identify the neighbor mesh point/face that produced the
39
+ minimum — the culprit location for a verdict, `show_component`, or a future
40
+ dimension-line highlight (task 14).
41
+ """
42
+
43
+ min_radial_mm: float = Field(ge=0)
44
+ point: Point3D
45
+ face_index: int = Field(ge=0)
46
+
47
+
48
+ def radial_distance(
49
+ axis: AxisLine,
50
+ mesh: ComponentMesh,
51
+ *,
52
+ z_min_mm: float,
53
+ z_max_mm: float,
54
+ ) -> RadialDistance | None:
55
+ """Minimum radial distance from `axis` to `mesh` points inside the axial zone.
56
+
57
+ Zone bounds are explicit caller-supplied parameters, not constants baked into this
58
+ module (GPC1): `z_min_mm`/`z_max_mm` are axial coordinates measured along
59
+ `axis.direction` from `axis.origin` — for a standard library fastener, `origin` is
60
+ the reference plane under the head/nut (see `AxisLine` docstring), but the sign of
61
+ "up" along the axis is not guaranteed, so this function does not assume it. Picking
62
+ the actual bounds (e.g. nut/head height plus a margin for wrench thickness) is the
63
+ caller's job, not this module's — see `core/capabilities/geometry_3d.py` (`measure_radial_distance`).
64
+
65
+ Geometry: for every mesh point `p`, project the vector `p - axis.origin` onto the
66
+ *unit* axis direction to get the axial coordinate `t` (`AxisLine.direction` is not
67
+ normalized by the bridge adapter — this function normalizes it itself, per the
68
+ `AxisLine` docstring). Points with `t` outside `[z_min_mm, z_max_mm]` are outside the
69
+ wrench zone and are skipped — this is how a same-axis bolt hole or washers get
70
+ excluded from the radial minimum, which is the whole point of the zone filter.
71
+ For points inside the zone, the radial component is
72
+ `(p - axis.origin) - t * unit_direction`; its norm is the radial distance that
73
+ ГОСТ 13682-80 Черт. 5 actually norms (K/R), not a surface-to-surface distance.
74
+
75
+ Returns the point with the smallest radial distance among the ones inside the zone,
76
+ or `None` if no mesh point falls inside the zone (empty zone — e.g. a neighbor
77
+ entirely outside the wrench's axial reach, or a mesh with no points at all).
78
+
79
+ Raises:
80
+ ValueError: `z_min_mm > z_max_mm`, or `axis.direction` has zero length (a
81
+ degenerate axis is a caller bug, not a legitimate "no data" case).
82
+ """
83
+ if z_min_mm > z_max_mm:
84
+ raise ValueError(f"z_min_mm ({z_min_mm:g}) must be <= z_max_mm ({z_max_mm:g})")
85
+ unit = unit_axis_direction(axis)
86
+
87
+ best: RadialDistance | None = None
88
+ for face in mesh.faces:
89
+ for point in face.points:
90
+ candidate = _radial_candidate_in_zone(point, axis, unit, (z_min_mm, z_max_mm), face.face_index)
91
+ if candidate is None:
92
+ continue
93
+ if best is None or candidate.min_radial_mm < best.min_radial_mm:
94
+ best = candidate
95
+ return best
96
+
97
+
98
+ def _radial_candidate_in_zone(
99
+ point: Point3D,
100
+ axis: AxisLine,
101
+ unit: tuple[float, float, float],
102
+ zone: tuple[float, float],
103
+ face_index: int,
104
+ ) -> RadialDistance | None:
105
+ z_min_mm, z_max_mm = zone
106
+ axial_mm, radial_mm = axial_and_radial_mm(point, axis, unit)
107
+ if axial_mm < z_min_mm or axial_mm > z_max_mm:
108
+ return None
109
+ return RadialDistance(min_radial_mm=radial_mm, point=point, face_index=face_index)
110
+
111
+
112
+ def radial_distance_from_exact_faces(
113
+ facts: Sequence[PlanarFaceFact | CylindricalFaceFact],
114
+ axis: AxisLine,
115
+ *,
116
+ z_min_mm: float,
117
+ z_max_mm: float,
118
+ ) -> RadialDistance | None:
119
+ """Minimum radial distance from `axis` among exact B-rep facts whose face overlaps the zone.
120
+
121
+ Fast-path counterpart of `radial_distance` (mesh vertices): `facts` come from
122
+ `GeometryInspector.component_planar_faces`/`component_cylindrical_faces` called with
123
+ a NEIGHBOR's identifier and the FASTENER's `axis` — each `fact.radial_mm` is already
124
+ the exact (no tessellation) perpendicular distance from `axis` to that whole face
125
+ (see those DTOs' docstrings — true for a planar face parallel to `axis`, and for a
126
+ cylindrical face whose own axis is parallel to `axis`, in both cases independent of
127
+ which point on the face or axis is picked).
128
+
129
+ The root-cause fix over `radial_distance`: a face is a candidate if its OWN AXIAL
130
+ EXTENT (the union of its `corners` projected onto `axis`, same projection as
131
+ `hex_detect.find_hex_prisms`) OVERLAPS `[z_min_mm, z_max_mm]` as an INTERVAL, not
132
+ "does some sampled point happen to land inside the zone". A real wall's corners
133
+ almost always span well beyond a narrow wrench-height zone on both ends, so interval
134
+ overlap reliably catches it; point-sampling (mesh vertices, only at a face's own
135
+ tessellation nodes) is what produced the honest-but-unlucky `empty_zone` this
136
+ function replaces as the primary path.
137
+
138
+ Returns the fact with the smallest `radial_mm` among overlapping candidates, or
139
+ `None` if none of `facts` overlaps the zone (a neighbor with no exact planar/
140
+ cylindrical wall near the fastener's axis in this height range — genuinely no B-rep
141
+ fast-path data; caller should fall back to `radial_distance` over the mesh).
142
+
143
+ Raises:
144
+ ValueError: `z_min_mm > z_max_mm` (a caller bug, not legitimate "no data").
145
+ """
146
+ if z_min_mm > z_max_mm:
147
+ raise ValueError(f"z_min_mm ({z_min_mm:g}) must be <= z_max_mm ({z_max_mm:g})")
148
+ unit = unit_axis_direction(axis)
149
+
150
+ best: RadialDistance | None = None
151
+ for fact in facts:
152
+ if not fact.corners:
153
+ continue
154
+ axial_values = [axial_and_radial_mm(corner, axis, unit)[0] for corner in fact.corners]
155
+ face_z_min = min(axial_values)
156
+ face_z_max = max(axial_values)
157
+ if face_z_max < z_min_mm or face_z_min > z_max_mm:
158
+ continue # face's axial extent doesn't reach the query zone at all
159
+ if best is None or fact.radial_mm < best.min_radial_mm:
160
+ best = RadialDistance(min_radial_mm=fact.radial_mm, point=fact.corners[0], face_index=fact.face_index)
161
+ return best
@@ -0,0 +1,9 @@
1
+ """Guarded KsAPI automation capability."""
2
+
3
+ from kompas_kernel.features.ksapi_automation.runner import (
4
+ GateRejectedError,
5
+ KsapiAutomationRunner,
6
+ KsapiCodeRunner,
7
+ )
8
+
9
+ __all__ = ["GateRejectedError", "KsapiAutomationRunner", "KsapiCodeRunner"]