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.
- kompas_kernel/__init__.py +3 -0
- kompas_kernel/features/__init__.py +9 -0
- kompas_kernel/features/assembly_navigator/__init__.py +8 -0
- kompas_kernel/features/assembly_navigator/commands.py +37 -0
- kompas_kernel/features/assembly_navigator/ksapi/__init__.py +5 -0
- kompas_kernel/features/assembly_navigator/ksapi/mapping.py +122 -0
- kompas_kernel/features/assembly_navigator/ksapi/presentation.py +107 -0
- kompas_kernel/features/assembly_navigator/ksapi/raw.py +74 -0
- kompas_kernel/features/assembly_navigator/ksapi/reader.py +287 -0
- kompas_kernel/features/assembly_navigator/ksapi/resolver.py +35 -0
- kompas_kernel/features/assembly_navigator/models.py +218 -0
- kompas_kernel/features/assembly_navigator/queries.py +42 -0
- kompas_kernel/features/assembly_navigator/service.py +31 -0
- kompas_kernel/features/assembly_navigator/summary.py +90 -0
- kompas_kernel/features/document_session/__init__.py +5 -0
- kompas_kernel/features/document_session/checkpointed.py +165 -0
- kompas_kernel/features/document_session/commands.py +94 -0
- kompas_kernel/features/document_session/digest.py +126 -0
- kompas_kernel/features/document_session/ksapi/__init__.py +5 -0
- kompas_kernel/features/document_session/ksapi/context_reader.py +174 -0
- kompas_kernel/features/document_session/ksapi/screenshot.py +365 -0
- kompas_kernel/features/document_session/ksapi/snapshot.py +181 -0
- kompas_kernel/features/document_session/models.py +311 -0
- kompas_kernel/features/document_session/queries.py +62 -0
- kompas_kernel/features/document_session/registry.py +101 -0
- kompas_kernel/features/document_session/service.py +128 -0
- kompas_kernel/features/geometry_3d/__init__.py +9 -0
- kompas_kernel/features/geometry_3d/axis_math.py +50 -0
- kompas_kernel/features/geometry_3d/commands.py +68 -0
- kompas_kernel/features/geometry_3d/hex_detect.py +113 -0
- kompas_kernel/features/geometry_3d/ksapi/__init__.py +5 -0
- kompas_kernel/features/geometry_3d/ksapi/component_geometry.py +559 -0
- kompas_kernel/features/geometry_3d/ksapi/measurements.py +368 -0
- kompas_kernel/features/geometry_3d/ksapi/presentation.py +204 -0
- kompas_kernel/features/geometry_3d/models.py +216 -0
- kompas_kernel/features/geometry_3d/queries.py +96 -0
- kompas_kernel/features/geometry_3d/service.py +69 -0
- kompas_kernel/features/geometry_3d/wrench_zone.py +161 -0
- kompas_kernel/features/ksapi_automation/__init__.py +9 -0
- kompas_kernel/features/ksapi_automation/gate.py +282 -0
- kompas_kernel/features/ksapi_automation/models.py +75 -0
- kompas_kernel/features/ksapi_automation/namespace.py +12 -0
- kompas_kernel/features/ksapi_automation/run_python.py +133 -0
- kompas_kernel/features/ksapi_automation/runner.py +126 -0
- kompas_kernel/features/ksapi_automation/service.py +85 -0
- kompas_kernel/features/part_authoring/__init__.py +5 -0
- kompas_kernel/features/part_authoring/build_geometry.py +122 -0
- kompas_kernel/features/part_authoring/models.py +99 -0
- kompas_kernel/features/wrench_clearance/__init__.py +7 -0
- kompas_kernel/features/wrench_clearance/models.py +87 -0
- kompas_kernel/features/wrench_clearance/service.py +154 -0
- kompas_kernel/kompas/__init__.py +14 -0
- kompas_kernel/kompas/errors.py +11 -0
- kompas_kernel/kompas/models.py +68 -0
- kompas_kernel/kompas/objects.py +93 -0
- kompas_kernel/kompas/platform.py +40 -0
- kompas_kernel/kompas/runtime.py +186 -0
- kompas_kernel/kompas/session.py +195 -0
- kompas_kernel/kompas/units.py +48 -0
- kompas_kernel/ksapi_static/__init__.py +8 -0
- kompas_kernel/ksapi_static/facts.py +132 -0
- kompas_kernel/ksapi_static/inventory.py +185 -0
- kompas_kernel/ksapi_static/recipes.py +114 -0
- kompas_kernel/ksapi_static/wrapper_grep.py +83 -0
- kompas_kernel/observability/__init__.py +1 -0
- kompas_kernel/observability/logger.py +24 -0
- kompas_kernel/observability/setup.py +115 -0
- kompas_kernel/observability/taxonomy.py +27 -0
- kompas_kernel/py.typed +1 -0
- kompas_kernel/standards/__init__.py +1 -0
- kompas_kernel/standards/wrench_clearance.py +234 -0
- kompas_kernel/utils/__init__.py +1 -0
- kompas_kernel/utils/filesystem.py +82 -0
- kompas_kernel/utils/validation.py +21 -0
- kompas_kernel-0.0.1.dist-info/METADATA +56 -0
- kompas_kernel-0.0.1.dist-info/RECORD +77 -0
- 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
|