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,368 @@
|
|
|
1
|
+
"""Distance/neighbor/ray-pick measurements over `IMeasurement3D` and `PickModelObjectsWithLimit`.
|
|
2
|
+
|
|
3
|
+
Free functions taking explicit `modules`/`app` dependencies (no adapter `self` state) —
|
|
4
|
+
same shape as `component_geometry.py`. Component identifier resolution
|
|
5
|
+
(`resolve_component`/`component_by_identifier`) is imported from there, not duplicated
|
|
6
|
+
(E16.04a dedup step).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from kompas_kernel.features.geometry_3d.ksapi.component_geometry import (
|
|
16
|
+
bool_xyz_point,
|
|
17
|
+
component_by_identifier,
|
|
18
|
+
resolve_component,
|
|
19
|
+
)
|
|
20
|
+
from kompas_kernel.features.geometry_3d.models import DistanceMeasurement, NeighborHit, NeighborsResult, RayHit
|
|
21
|
+
from kompas_kernel.kompas.errors import KompasError
|
|
22
|
+
from kompas_kernel.kompas.models import Point3D
|
|
23
|
+
from kompas_kernel.kompas.objects import as_document_3d, constants3d_class
|
|
24
|
+
from kompas_kernel.kompas.runtime import KsApiModules
|
|
25
|
+
from kompas_kernel.observability.logger import get_logger
|
|
26
|
+
from kompas_kernel.observability.taxonomy import LogDomain, LogEventType, event_name
|
|
27
|
+
|
|
28
|
+
logger = get_logger(__name__)
|
|
29
|
+
|
|
30
|
+
_KSAPI_BOOL_XYZ_VALUE_COUNT = 4
|
|
31
|
+
_POINT3D_COORDINATE_COUNT = 3
|
|
32
|
+
_PART_ORIGIN_MATCH_TOLERANCE_MM = 1e-6
|
|
33
|
+
_GABARIT_VALUE_COUNT = 7 # (ok, x1, y1, z1, x2, y2, z2)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def measure_min_distance(
|
|
37
|
+
modules: KsApiModules,
|
|
38
|
+
app: Any,
|
|
39
|
+
component_identifier_1: str,
|
|
40
|
+
component_identifier_2: str,
|
|
41
|
+
) -> DistanceMeasurement:
|
|
42
|
+
"""Measure minimum distance between two top-level components in one call.
|
|
43
|
+
|
|
44
|
+
Transient `IMeasurement3D` (`IPart.GetMeasurement3D()`), spike 11: computes
|
|
45
|
+
`IPart <-> IPart` directly (unlike `IDistanceAngleMeasurement3D`, which spike 02
|
|
46
|
+
found only computes `IFace <-> IFace` and needed a 37-54-call face-pair perimeter
|
|
47
|
+
per component pair). `IMeasurement3D` is an `IKompasAPIObject`, not an
|
|
48
|
+
`IModelObject` — no `Add()`/`Delete()`, nothing enters a document collection.
|
|
49
|
+
The measurement object itself is cached on `IPart` (`GetMeasurement3D()` returns
|
|
50
|
+
the same reference on repeated calls), but each call here still does a
|
|
51
|
+
fresh
|
|
52
|
+
`SetObject1/2` + `Calculate()`, so the result is always freshly computed for the
|
|
53
|
+
requested pair.
|
|
54
|
+
"""
|
|
55
|
+
_doc3d, top, components, component_1 = resolve_component(
|
|
56
|
+
modules, app, "measure_min_distance", component_identifier_1
|
|
57
|
+
)
|
|
58
|
+
component_2 = component_by_identifier("measure_min_distance", components, component_identifier_2)
|
|
59
|
+
constants3d = constants3d_class(modules)
|
|
60
|
+
|
|
61
|
+
measurement = top.GetMeasurement3D()
|
|
62
|
+
if not measurement:
|
|
63
|
+
raise KompasError("measure_min_distance: IPart.GetMeasurement3D() is unavailable")
|
|
64
|
+
|
|
65
|
+
measurement.SetObject1(component_1)
|
|
66
|
+
measurement.SetObject2(component_2)
|
|
67
|
+
started_at = time.perf_counter()
|
|
68
|
+
result_code = int(measurement.Calculate())
|
|
69
|
+
elapsed_s = time.perf_counter() - started_at
|
|
70
|
+
|
|
71
|
+
# ksMResUnknown=0 (constants3d.py) is the documented "not computed" result
|
|
72
|
+
# (spike 02: IPart<->IPart via IDistanceAngleMeasurement3D stuck at exactly this
|
|
73
|
+
# code when it couldn't calculate). Every other code (ksMResCalculate=-1, or one
|
|
74
|
+
# of the specific axis/surface relationship codes 1-13) means Calculate() produced
|
|
75
|
+
# a real answer — spike 11 saw -1 for part/body pairs and 2 (AxisAxisParallel) for
|
|
76
|
+
# a face-level calibration pair, both valid.
|
|
77
|
+
if result_code == int(constants3d.ksMResUnknown):
|
|
78
|
+
raise KompasError(
|
|
79
|
+
"measure_min_distance: IMeasurement3D.Calculate() could not determine a distance for "
|
|
80
|
+
f"components {component_identifier_1!r} and {component_identifier_2!r} (ksMResUnknown)"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Spike 11 flagged result "stickiness" on a failing pair as unverified — validate
|
|
84
|
+
# Lmin/points defensively instead of trusting a non-zero result code alone.
|
|
85
|
+
distance_mm = float(measurement.GetLmin())
|
|
86
|
+
if not math.isfinite(distance_mm) or distance_mm < 0:
|
|
87
|
+
raise KompasError(
|
|
88
|
+
f"measure_min_distance: IMeasurement3D returned invalid Lmin={distance_mm!r} for "
|
|
89
|
+
f"components {component_identifier_1!r} and {component_identifier_2!r} "
|
|
90
|
+
f"(Calculate() code={result_code})"
|
|
91
|
+
)
|
|
92
|
+
point1 = bool_xyz_point(measurement.GetMinPoint1(0.0, 0.0, 0.0))
|
|
93
|
+
point2 = bool_xyz_point(measurement.GetMinPoint2(0.0, 0.0, 0.0))
|
|
94
|
+
if point1 is None or point2 is None:
|
|
95
|
+
raise KompasError(
|
|
96
|
+
"measure_min_distance: IMeasurement3D did not return valid min-distance points for "
|
|
97
|
+
f"components {component_identifier_1!r} and {component_identifier_2!r} "
|
|
98
|
+
f"(Calculate() code={result_code})"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
logger.info(
|
|
102
|
+
event_name(LogDomain.BRIDGE, "ksapi_measure_min_distance", LogEventType.COMPLETED),
|
|
103
|
+
component_identifier_1=component_identifier_1,
|
|
104
|
+
component_identifier_2=component_identifier_2,
|
|
105
|
+
distance_mm=distance_mm,
|
|
106
|
+
measure_result_code=result_code,
|
|
107
|
+
elapsed_s=elapsed_s,
|
|
108
|
+
)
|
|
109
|
+
return DistanceMeasurement(
|
|
110
|
+
component_identifier_1=component_identifier_1,
|
|
111
|
+
component_identifier_2=component_identifier_2,
|
|
112
|
+
distance_mm=distance_mm,
|
|
113
|
+
point1=point1,
|
|
114
|
+
point2=point2,
|
|
115
|
+
measure_result_code=result_code,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _component_bbox(
|
|
120
|
+
component: Any,
|
|
121
|
+
) -> tuple[float, float, float, float, float, float] | None:
|
|
122
|
+
"""Axis-aligned bbox компонента (min/max по осям, мм), или `None` если габарит недоступен."""
|
|
123
|
+
raw = component.GetGabarit(True, True, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
|
124
|
+
values = tuple(raw) if raw is not None else ()
|
|
125
|
+
if len(values) < _GABARIT_VALUE_COUNT or not bool(values[0]):
|
|
126
|
+
return None
|
|
127
|
+
x1, y1, z1, x2, y2, z2 = (float(value) for value in values[-6:])
|
|
128
|
+
return (min(x1, x2), min(y1, y2), min(z1, z2), max(x1, x2), max(y1, y2), max(z1, z2))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _aabb_gap_mm(
|
|
132
|
+
box_a: tuple[float, float, float, float, float, float],
|
|
133
|
+
box_b: tuple[float, float, float, float, float, float],
|
|
134
|
+
) -> float:
|
|
135
|
+
"""Евклидов зазор между двумя AABB (0.0 если пересекаются).
|
|
136
|
+
|
|
137
|
+
Нижняя оценка расстояния между телами — дешёвый предфильтр перед точным
|
|
138
|
+
`IMeasurement3D`: если даже коробки дальше порога, сами тела тем более дальше.
|
|
139
|
+
"""
|
|
140
|
+
total = 0.0
|
|
141
|
+
for axis in range(_POINT3D_COORDINATE_COUNT):
|
|
142
|
+
axis_gap = max(box_b[axis] - box_a[axis + 3], box_a[axis] - box_b[axis + 3], 0.0)
|
|
143
|
+
total += axis_gap * axis_gap
|
|
144
|
+
return math.sqrt(total)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _try_measure_pair(
|
|
148
|
+
measurement: Any, constants3d: Any, object_1: Any, object_2: Any
|
|
149
|
+
) -> tuple[float, Point3D, Point3D, int] | None:
|
|
150
|
+
"""Non-raising ядро замера пары: `(distance_mm, point1, point2, code)` или `None`.
|
|
151
|
+
|
|
152
|
+
Та же последовательность `IMeasurement3D`, что в `measure_min_distance`, но при
|
|
153
|
+
скане соседей неизмеримая/невалидная пара — не ошибка всего запроса, а честный
|
|
154
|
+
пропуск кандидата, поэтому возвращается `None`, а не бросается `KompasError`.
|
|
155
|
+
"""
|
|
156
|
+
measurement.SetObject1(object_1)
|
|
157
|
+
measurement.SetObject2(object_2)
|
|
158
|
+
result_code = int(measurement.Calculate())
|
|
159
|
+
if result_code == int(constants3d.ksMResUnknown):
|
|
160
|
+
return None
|
|
161
|
+
distance_mm = float(measurement.GetLmin())
|
|
162
|
+
if not math.isfinite(distance_mm) or distance_mm < 0:
|
|
163
|
+
return None
|
|
164
|
+
point_1 = bool_xyz_point(measurement.GetMinPoint1(0.0, 0.0, 0.0))
|
|
165
|
+
point_2 = bool_xyz_point(measurement.GetMinPoint2(0.0, 0.0, 0.0))
|
|
166
|
+
if point_1 is None or point_2 is None:
|
|
167
|
+
return None
|
|
168
|
+
return distance_mm, point_1, point_2, result_code
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _part_name(part: Any) -> str | None:
|
|
172
|
+
raw = part.GetName()
|
|
173
|
+
name = str(raw).strip() if raw is not None else ""
|
|
174
|
+
return name or None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def find_neighbors(modules: KsApiModules, app: Any, identifier: str, gap_mm: float) -> NeighborsResult:
|
|
178
|
+
"""Компоненты в пределах `gap_mm` от компонента `identifier` — bbox-предфильтр + линейка.
|
|
179
|
+
|
|
180
|
+
Детерминированный «что рядом с компонентом»: сначала bbox-предфильтр по `GetGabarit`
|
|
181
|
+
(чистая математика, отсекает заведомо далёкие компоненты без вызова замера), затем
|
|
182
|
+
точный зазор до каждого кандидата — транзиентный `IMeasurement3D` part↔part (спайк 11:
|
|
183
|
+
тот же механизм, что `measure_min_distance`, без объекта в дереве документа). Кандидаты,
|
|
184
|
+
чей ТОЧНЫЙ зазор всё же больше `gap_mm`, отбрасываются (bbox — только нижняя оценка).
|
|
185
|
+
Результат отсортирован по возрастанию зазора; сам компонент-цель в него не входит.
|
|
186
|
+
|
|
187
|
+
Read-only: ни `GetGabarit`, ни `IMeasurement3D` не создают объектов в дереве и не
|
|
188
|
+
трогают геометрию — в отличие от `ICollision` (спайк find-neighbors в
|
|
189
|
+
`docs/recipes/ksapi-link.md`), который сюда сознательно не берётся.
|
|
190
|
+
"""
|
|
191
|
+
if gap_mm <= 0:
|
|
192
|
+
raise KompasError("find_neighbors: gap_mm must be positive")
|
|
193
|
+
_doc3d, top, components, target = resolve_component(modules, app, "find_neighbors", identifier)
|
|
194
|
+
constants3d = constants3d_class(modules)
|
|
195
|
+
target_bbox = _component_bbox(target)
|
|
196
|
+
if target_bbox is None:
|
|
197
|
+
raise KompasError(f"find_neighbors: GetGabarit() failed for target component {identifier!r}")
|
|
198
|
+
|
|
199
|
+
measurement = top.GetMeasurement3D()
|
|
200
|
+
if not measurement:
|
|
201
|
+
raise KompasError("find_neighbors: IPart.GetMeasurement3D() is unavailable")
|
|
202
|
+
|
|
203
|
+
neighbors: list[NeighborHit] = []
|
|
204
|
+
bbox_candidate_count = 0
|
|
205
|
+
for index, component in enumerate(components):
|
|
206
|
+
if component is target:
|
|
207
|
+
continue
|
|
208
|
+
component_bbox = _component_bbox(component)
|
|
209
|
+
if component_bbox is None:
|
|
210
|
+
continue
|
|
211
|
+
if _aabb_gap_mm(target_bbox, component_bbox) > gap_mm:
|
|
212
|
+
continue # предфильтр: коробки дальше порога — точный замер не нужен
|
|
213
|
+
bbox_candidate_count += 1
|
|
214
|
+
measured = _try_measure_pair(measurement, constants3d, target, component)
|
|
215
|
+
if measured is None:
|
|
216
|
+
continue
|
|
217
|
+
distance_mm, point_on_target, point_on_neighbor, result_code = measured
|
|
218
|
+
if distance_mm > gap_mm:
|
|
219
|
+
continue
|
|
220
|
+
neighbors.append(
|
|
221
|
+
NeighborHit(
|
|
222
|
+
component_identifier=str(index),
|
|
223
|
+
component_name=_part_name(component),
|
|
224
|
+
distance_mm=distance_mm,
|
|
225
|
+
point_on_target=point_on_target,
|
|
226
|
+
point_on_neighbor=point_on_neighbor,
|
|
227
|
+
measure_result_code=result_code,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
neighbors.sort(key=lambda hit: hit.distance_mm)
|
|
231
|
+
|
|
232
|
+
logger.info(
|
|
233
|
+
event_name(LogDomain.BRIDGE, "ksapi_find_neighbors", LogEventType.COMPLETED),
|
|
234
|
+
identifier=identifier,
|
|
235
|
+
gap_mm=gap_mm,
|
|
236
|
+
bbox_candidates=bbox_candidate_count,
|
|
237
|
+
neighbors=len(neighbors),
|
|
238
|
+
)
|
|
239
|
+
return NeighborsResult(target_identifier=identifier, gap_mm=gap_mm, neighbors=neighbors)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _api_reference(obj: Any) -> int | None:
|
|
243
|
+
raw = getattr(obj, "reference", None)
|
|
244
|
+
if raw is None:
|
|
245
|
+
return None
|
|
246
|
+
try:
|
|
247
|
+
return int(raw)
|
|
248
|
+
except (TypeError, ValueError):
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _part_origin(part: Any) -> tuple[float, float, float] | None:
|
|
253
|
+
placement = part.GetPlacement()
|
|
254
|
+
if not placement:
|
|
255
|
+
return None
|
|
256
|
+
raw = placement.GetOrigin(0.0, 0.0, 0.0)
|
|
257
|
+
values = tuple(raw) if raw is not None else ()
|
|
258
|
+
if len(values) < _KSAPI_BOOL_XYZ_VALUE_COUNT or not bool(values[0]):
|
|
259
|
+
return None
|
|
260
|
+
return (float(values[1]), float(values[2]), float(values[3]))
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _same_origin(left: tuple[float, float, float], right: tuple[float, float, float]) -> bool:
|
|
264
|
+
return all(abs(a - b) <= _PART_ORIGIN_MATCH_TOLERANCE_MM for a, b in zip(left, right, strict=True))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _component_index_for_part(components: list[Any], target_part: Any) -> int | None:
|
|
268
|
+
"""Resolve a picked object's owning `IPart` to a top-level component index.
|
|
269
|
+
|
|
270
|
+
`IModelObject.GetPart()` returns a fresh wrapper, so object identity is not enough.
|
|
271
|
+
Prefer SWIG reference equality and fall back to placement origin, as confirmed by
|
|
272
|
+
spike 03 for picked face/edge/body hits.
|
|
273
|
+
"""
|
|
274
|
+
target_reference = _api_reference(target_part)
|
|
275
|
+
if target_reference is not None and target_reference != 0:
|
|
276
|
+
for index, component in enumerate(components):
|
|
277
|
+
if _api_reference(component) == target_reference:
|
|
278
|
+
return index
|
|
279
|
+
|
|
280
|
+
target_origin = _part_origin(target_part)
|
|
281
|
+
if target_origin is None:
|
|
282
|
+
return None
|
|
283
|
+
for index, component in enumerate(components):
|
|
284
|
+
component_origin = _part_origin(component)
|
|
285
|
+
if component_origin is not None and _same_origin(component_origin, target_origin):
|
|
286
|
+
return index
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _document_3d_ex(modules: KsApiModules, doc: Any, doc3d: Any) -> Any:
|
|
291
|
+
"""Resolve `IKompasDocument3DEx` from document or document3d handle."""
|
|
292
|
+
doc3d_ex = modules.ksapi.IKompasDocument3DEx(doc)
|
|
293
|
+
if not doc3d_ex:
|
|
294
|
+
doc3d_ex = modules.ksapi.IKompasDocument3DEx(doc3d)
|
|
295
|
+
if not doc3d_ex:
|
|
296
|
+
raise KompasError("probe_ray: IKompasDocument3DEx is unavailable")
|
|
297
|
+
return doc3d_ex
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _ray_hit_for_object(
|
|
301
|
+
modules: KsApiModules,
|
|
302
|
+
constants3d: Any,
|
|
303
|
+
components: list[Any],
|
|
304
|
+
obj: Any,
|
|
305
|
+
) -> RayHit | None:
|
|
306
|
+
"""Map one pick result to a component hit, or `None` if it is not a part hit."""
|
|
307
|
+
model_object = modules.ksapi.IModelObject(obj)
|
|
308
|
+
if not model_object:
|
|
309
|
+
return None
|
|
310
|
+
type_code = int(model_object.GetModelObjectType())
|
|
311
|
+
owning_part = model_object.GetPart()
|
|
312
|
+
if not owning_part and type_code == int(constants3d.o3d_part):
|
|
313
|
+
owning_part = modules.ksapi.IPart(model_object)
|
|
314
|
+
if not owning_part:
|
|
315
|
+
return None
|
|
316
|
+
component_index = _component_index_for_part(components, owning_part)
|
|
317
|
+
if component_index is None:
|
|
318
|
+
return None
|
|
319
|
+
component = components[component_index]
|
|
320
|
+
return RayHit(
|
|
321
|
+
hit=True,
|
|
322
|
+
component_identifier=str(component_index),
|
|
323
|
+
component_name=_part_name(component),
|
|
324
|
+
model_object_type=type_code,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def probe_ray(
|
|
329
|
+
modules: KsApiModules,
|
|
330
|
+
app: Any,
|
|
331
|
+
origin: Point3D,
|
|
332
|
+
direction: Point3D,
|
|
333
|
+
limit_mm: float,
|
|
334
|
+
) -> RayHit:
|
|
335
|
+
"""Pick model objects along a geometric ray and return only owning component.
|
|
336
|
+
|
|
337
|
+
`PickModelObjectsWithLimit` returns intersection points, but spike 03 found
|
|
338
|
+
placeholders at body/part levels. This method intentionally ignores them.
|
|
339
|
+
"""
|
|
340
|
+
if limit_mm < 0:
|
|
341
|
+
raise KompasError("probe_ray: limit_mm must be non-negative")
|
|
342
|
+
direction_length_sq = direction.x_mm**2 + direction.y_mm**2 + direction.z_mm**2
|
|
343
|
+
if direction_length_sq == 0:
|
|
344
|
+
raise KompasError("probe_ray: direction vector must be non-zero")
|
|
345
|
+
|
|
346
|
+
doc = app.GetActiveDocument()
|
|
347
|
+
doc3d = as_document_3d(modules, doc)
|
|
348
|
+
if doc3d is None:
|
|
349
|
+
raise KompasError("probe_ray: активный документ не 3D")
|
|
350
|
+
constants3d = constants3d_class(modules)
|
|
351
|
+
components: list[Any] = list(doc3d.GetTopPart().GetPartsArray(int(constants3d.ksAllParts)))
|
|
352
|
+
doc3d_ex = _document_3d_ex(modules, doc, doc3d)
|
|
353
|
+
|
|
354
|
+
pick_ray = [
|
|
355
|
+
origin.x_mm,
|
|
356
|
+
origin.y_mm,
|
|
357
|
+
origin.z_mm,
|
|
358
|
+
direction.x_mm,
|
|
359
|
+
direction.y_mm,
|
|
360
|
+
direction.z_mm,
|
|
361
|
+
]
|
|
362
|
+
objects, _intersection_points = doc3d_ex.PickModelObjectsWithLimit(pick_ray, float(limit_mm), None, [])
|
|
363
|
+
for obj in objects:
|
|
364
|
+
hit = _ray_hit_for_object(modules, constants3d, components, obj)
|
|
365
|
+
if hit is not None:
|
|
366
|
+
return hit
|
|
367
|
+
|
|
368
|
+
return RayHit(hit=False)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Permanent dimension-line presentation: `show_distance_dimension`/`clear_dimensions`.
|
|
2
|
+
|
|
3
|
+
Free functions taking explicit `modules`/`app` dependencies (no adapter `self` state) —
|
|
4
|
+
same shape as `component_geometry.py`/`measurements.py`. Component identifier resolution
|
|
5
|
+
(`resolve_component`/`component_by_identifier`) and face resolution (`nearest_face`) are
|
|
6
|
+
imported from `component_geometry.py`, not duplicated (E16.04a dedup step).
|
|
7
|
+
|
|
8
|
+
Only the generated wrapper's stable integer reference is retained between callbacks (in
|
|
9
|
+
the caller's `shown_distance_measurement_references` list); the raw measurement wrapper
|
|
10
|
+
stays local to this callback — no raw KsAPI handle leaves `KompasSession.run(...)`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from kompas_kernel.features.geometry_3d.ksapi.component_geometry import (
|
|
18
|
+
bool_xyz_point,
|
|
19
|
+
component_by_identifier,
|
|
20
|
+
nearest_face,
|
|
21
|
+
resolve_component,
|
|
22
|
+
)
|
|
23
|
+
from kompas_kernel.features.geometry_3d.models import ShowDistanceResult
|
|
24
|
+
from kompas_kernel.kompas.errors import KompasError
|
|
25
|
+
from kompas_kernel.kompas.objects import as_document_3d, constants3d_class, constants_class
|
|
26
|
+
from kompas_kernel.kompas.runtime import KsApiModules
|
|
27
|
+
from kompas_kernel.observability.logger import get_logger
|
|
28
|
+
from kompas_kernel.observability.taxonomy import LogDomain, LogEventType, event_name
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def show_distance_dimension(
|
|
34
|
+
modules: KsApiModules,
|
|
35
|
+
app: Any,
|
|
36
|
+
component_identifier_1: str,
|
|
37
|
+
component_identifier_2: str,
|
|
38
|
+
shown_distance_measurement_references: list[int],
|
|
39
|
+
) -> ShowDistanceResult:
|
|
40
|
+
"""Measure the fast fact, then leave a PERMANENT dimension line visible in 3D.
|
|
41
|
+
|
|
42
|
+
Fast fact: transient `IMeasurement3D` (`IPart.GetMeasurement3D()`, same mechanism
|
|
43
|
+
as `measure_min_distance`). The permanent highlight uses the OTHER measurement
|
|
44
|
+
object (`IDistanceAngleMeasurement3D` via `IMeasurementContainer`, spike 02),
|
|
45
|
+
which computes only `IFace<->IFace` — so the face pair is resolved by nearest
|
|
46
|
+
tessellated vertex to each component's min-distance point (`nearest_face`).
|
|
47
|
+
|
|
48
|
+
Not read-only: creates a real `IModelObject` in the document's
|
|
49
|
+
`IDistanceAngleMeasurements3D` collection and does NOT delete it (unlike
|
|
50
|
+
`measure_min_distance`'s transient object, which never enters a collection) — the
|
|
51
|
+
whole point is a persistent, user-visible dimension line. `IKompasDocument.
|
|
52
|
+
IsChanged()` will flip to `True` (spike 02 fact) even though geometry is untouched.
|
|
53
|
+
Do NOT save the document while a shown feature exists; call
|
|
54
|
+
`clear_dimensions()` (or delete manually in KOMPAS) when done.
|
|
55
|
+
|
|
56
|
+
Only the generated wrapper's stable integer reference is retained between
|
|
57
|
+
callbacks; the raw measurement wrapper stays local to this callback.
|
|
58
|
+
"""
|
|
59
|
+
doc3d, top, components, component_1 = resolve_component(
|
|
60
|
+
modules, app, "show_distance_dimension", component_identifier_1
|
|
61
|
+
)
|
|
62
|
+
component_2 = component_by_identifier("show_distance_dimension", components, component_identifier_2)
|
|
63
|
+
constants = constants_class(modules)
|
|
64
|
+
constants3d = constants3d_class(modules)
|
|
65
|
+
|
|
66
|
+
fast_measurement = top.GetMeasurement3D()
|
|
67
|
+
if not fast_measurement:
|
|
68
|
+
raise KompasError("show_distance_dimension: IPart.GetMeasurement3D() is unavailable")
|
|
69
|
+
fast_measurement.SetObject1(component_1)
|
|
70
|
+
fast_measurement.SetObject2(component_2)
|
|
71
|
+
result_code = int(fast_measurement.Calculate())
|
|
72
|
+
if result_code == int(constants3d.ksMResUnknown):
|
|
73
|
+
raise KompasError(
|
|
74
|
+
"show_distance_dimension: fast measurement could not determine a distance for "
|
|
75
|
+
f"components {component_identifier_1!r} and {component_identifier_2!r} (ksMResUnknown)"
|
|
76
|
+
)
|
|
77
|
+
distance_mm = float(fast_measurement.GetLmin())
|
|
78
|
+
point1 = bool_xyz_point(fast_measurement.GetMinPoint1(0.0, 0.0, 0.0))
|
|
79
|
+
point2 = bool_xyz_point(fast_measurement.GetMinPoint2(0.0, 0.0, 0.0))
|
|
80
|
+
if point1 is None or point2 is None:
|
|
81
|
+
raise KompasError(
|
|
82
|
+
"show_distance_dimension: fast measurement did not return valid min-distance points for "
|
|
83
|
+
f"components {component_identifier_1!r} and {component_identifier_2!r}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
face_1 = nearest_face(modules, component_1, top, point1)
|
|
87
|
+
face_2 = nearest_face(modules, component_2, top, point2)
|
|
88
|
+
if face_1 is None or face_2 is None:
|
|
89
|
+
raise KompasError(
|
|
90
|
+
"show_distance_dimension: could not resolve a tessellated face near the min-distance point "
|
|
91
|
+
f"for components {component_identifier_1!r}/{component_identifier_2!r}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
measurement_container = modules.ksapi.IMeasurementContainer(top)
|
|
95
|
+
if not measurement_container:
|
|
96
|
+
raise KompasError("show_distance_dimension: IMeasurementContainer is unavailable")
|
|
97
|
+
distance_measurements = measurement_container.GetDistanceAngleMeasurements3D()
|
|
98
|
+
if distance_measurements is None:
|
|
99
|
+
raise KompasError("show_distance_dimension: GetDistanceAngleMeasurements3D() is unavailable")
|
|
100
|
+
permanent_measurement = distance_measurements.Add()
|
|
101
|
+
if not permanent_measurement:
|
|
102
|
+
raise KompasError("show_distance_dimension: IDistanceAngleMeasurements3D.Add() failed")
|
|
103
|
+
permanent_measurement.SetObject1(face_1)
|
|
104
|
+
permanent_measurement.SetObject2(face_2)
|
|
105
|
+
permanent_measurement.Update()
|
|
106
|
+
shown_value_mm = float(permanent_measurement.GetLmin())
|
|
107
|
+
measurement_reference = int(modules.ksapi.GetReference(permanent_measurement))
|
|
108
|
+
if measurement_reference == 0:
|
|
109
|
+
raise KompasError("show_distance_dimension: created measurement has no stable reference")
|
|
110
|
+
shown_distance_measurement_references.append(measurement_reference)
|
|
111
|
+
|
|
112
|
+
sm = doc3d.GetSelectionManager()
|
|
113
|
+
sm.UnselectAll()
|
|
114
|
+
# Try selecting the measurement feature itself first (variant 2 — see task 14
|
|
115
|
+
# brief); if KOMPAS doesn't accept it as a selectable object, fall back to
|
|
116
|
+
# zooming the second (culprit) component so the user still lands near the gap.
|
|
117
|
+
feature_selected = bool(sm.Select([permanent_measurement]))
|
|
118
|
+
selected = feature_selected or bool(sm.Select([component_2]))
|
|
119
|
+
zoomed = bool(app.ExecuteKompasCommand(int(constants.ksCMZoomSelected), True)) if selected else False
|
|
120
|
+
|
|
121
|
+
logger.info(
|
|
122
|
+
event_name(LogDomain.BRIDGE, "ksapi_show_distance_dimension", LogEventType.COMPLETED),
|
|
123
|
+
component_identifier_1=component_identifier_1,
|
|
124
|
+
component_identifier_2=component_identifier_2,
|
|
125
|
+
distance_mm=distance_mm,
|
|
126
|
+
shown_value_mm=shown_value_mm,
|
|
127
|
+
feature_selected=feature_selected,
|
|
128
|
+
zoomed=zoomed,
|
|
129
|
+
)
|
|
130
|
+
return ShowDistanceResult(
|
|
131
|
+
distance_mm=distance_mm,
|
|
132
|
+
point1=point1,
|
|
133
|
+
point2=point2,
|
|
134
|
+
zoomed=zoomed,
|
|
135
|
+
shown_value_mm=shown_value_mm,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def clear_dimensions(
|
|
140
|
+
modules: KsApiModules,
|
|
141
|
+
app: Any,
|
|
142
|
+
shown_distance_measurement_references: list[int],
|
|
143
|
+
) -> tuple[int, list[int]]:
|
|
144
|
+
"""Delete ONLY the permanent measurement features created by `show_distance_dimension`.
|
|
145
|
+
|
|
146
|
+
Never touches `IDistanceAngleMeasurements3D` entries the user created themselves —
|
|
147
|
+
only the integer references the caller's list holds. Returns `(deleted, remaining)`; the
|
|
148
|
+
caller (`GeometryService`/`commands.clear_dimensions`) is the one that reassigns its own
|
|
149
|
+
tracking state to `remaining` (a plain function cannot rebind the caller's attribute —
|
|
150
|
+
see `commands.py::ShownDimensionsState`).
|
|
151
|
+
|
|
152
|
+
Two failure modes, both logged and both kept in `remaining` so a retry or manual
|
|
153
|
+
cleanup is still possible:
|
|
154
|
+
* the reference does not resolve in the ACTIVE document — the document was switched
|
|
155
|
+
after `show_distance_dimension`, so the feature is still alive in the other one;
|
|
156
|
+
* `IFeature.Delete()` returned `False` for a resolved measurement.
|
|
157
|
+
"""
|
|
158
|
+
doc3d = as_document_3d(modules, app.GetActiveDocument())
|
|
159
|
+
if doc3d is None:
|
|
160
|
+
raise KompasError("clear_dimensions: активный документ не 3D")
|
|
161
|
+
measurement_container = modules.ksapi.IMeasurementContainer(doc3d.GetTopPart())
|
|
162
|
+
if not measurement_container:
|
|
163
|
+
raise KompasError("clear_dimensions: IMeasurementContainer is unavailable")
|
|
164
|
+
measurements = measurement_container.GetDistanceAngleMeasurements3D()
|
|
165
|
+
if measurements is None:
|
|
166
|
+
raise KompasError("clear_dimensions: GetDistanceAngleMeasurements3D() is unavailable")
|
|
167
|
+
by_reference: dict[int, Any] = {}
|
|
168
|
+
for index in range(int(measurements.GetCount())):
|
|
169
|
+
measurement = measurements.GetDistanceAngleMeasurement3D(index)
|
|
170
|
+
if measurement is None:
|
|
171
|
+
continue
|
|
172
|
+
reference = int(modules.ksapi.GetReference(measurement))
|
|
173
|
+
if reference != 0:
|
|
174
|
+
by_reference[reference] = measurement
|
|
175
|
+
|
|
176
|
+
remaining: list[int] = []
|
|
177
|
+
deleted = 0
|
|
178
|
+
for reference in shown_distance_measurement_references:
|
|
179
|
+
measurement = by_reference.get(reference)
|
|
180
|
+
if measurement is None:
|
|
181
|
+
remaining.append(reference)
|
|
182
|
+
logger.warning(
|
|
183
|
+
event_name(LogDomain.BRIDGE, "ksapi_clear_dimensions", LogEventType.FAILED),
|
|
184
|
+
reason="reference not found in the active document",
|
|
185
|
+
reference=reference,
|
|
186
|
+
)
|
|
187
|
+
continue
|
|
188
|
+
feature = modules.ksapi.IFeature(measurement)
|
|
189
|
+
ok = bool(feature.Delete()) if feature else False
|
|
190
|
+
if ok:
|
|
191
|
+
deleted += 1
|
|
192
|
+
else:
|
|
193
|
+
remaining.append(reference)
|
|
194
|
+
logger.warning(
|
|
195
|
+
event_name(LogDomain.BRIDGE, "ksapi_clear_dimensions", LogEventType.FAILED),
|
|
196
|
+
reason="IFeature.Delete() returned False",
|
|
197
|
+
reference=reference,
|
|
198
|
+
)
|
|
199
|
+
logger.info(
|
|
200
|
+
event_name(LogDomain.BRIDGE, "ksapi_clear_dimensions", LogEventType.COMPLETED),
|
|
201
|
+
deleted=deleted,
|
|
202
|
+
remaining=len(remaining),
|
|
203
|
+
)
|
|
204
|
+
return deleted, remaining
|