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,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