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,3 @@
1
+ """Framework-neutral KOMPAS product kernel."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,9 @@
1
+ """Framework-free capability slices for live work with a KOMPAS document/assembly.
2
+
3
+ Kernel owns the framework-free feature slices that talk to a live `KompasSession`
4
+ (`document_session`, `geometry_3d`, `assembly_navigator`, `wrench_clearance`) and the
5
+ `run_python` sandbox-policy slices (`ksapi_automation`, `part_authoring`, E17.04e): DTO,
6
+ queries/commands, gate/namespace policy and their own `ksapi/` implementation, with no
7
+ Pydantic AI and no Copilot import. Agent wiring, tool surface and model-facing text stay
8
+ in Copilot (`kompas_copilot.core.capabilities`).
9
+ """
@@ -0,0 +1,8 @@
1
+ """Assembly navigator feature slice: read + show top-level КОМПАС-Сборка components.
2
+
3
+ Framework-free (no Pydantic AI, no `kompas_copilot.bridge`): owns its DTO (`models.py`),
4
+ raw KsAPI reader/resolver/presentation (`ksapi/`), public query/command API
5
+ (`queries.py`/`commands.py`) over a `KompasSession`, and a thin service holder
6
+ (`service.py`). Thin Pydantic AI registration lives in
7
+ `core/capabilities/assembly_navigator.py`.
8
+ """
@@ -0,0 +1,37 @@
1
+ """Framework-free public show/select command for the assembly navigator.
2
+
3
+ No Pydantic AI, no `kompas_copilot.bridge` import — raw KsAPI select+zoom lives in
4
+ `ksapi/presentation.py`; this module only validates the empty-list precondition and
5
+ wires it through one session callback.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from kompas_kernel.features.assembly_navigator.ksapi.presentation import select_components_and_zoom
11
+ from kompas_kernel.features.assembly_navigator.models import ComponentIdentifiers, ShowComponentsResult
12
+ from kompas_kernel.kompas.errors import KompasError
13
+ from kompas_kernel.kompas.session import KompasContext, KompasSession
14
+
15
+
16
+ async def show_components(session: KompasSession, identifiers: ComponentIdentifiers) -> ShowComponentsResult:
17
+ """Выделить компоненты и зумнуть вид по общему выделению (И3).
18
+
19
+ `identifiers` — непустой список индексов из `read_assembly`
20
+ (поле `AssemblyComponent.identifier`). Для одного компонента передаётся
21
+ список из одного элемента.
22
+
23
+ Все найденные цели передаются одним списком в `ISelectionManager.Select`, после
24
+ чего `ksCMZoomSelected` вызывается ровно один раз. Невалидный индекс отражается
25
+ как `found=False` и не мешает показать остальные найденные компоненты.
26
+ """
27
+ if not identifiers:
28
+ raise KompasError("show_component: передан пустой список идентификаторов")
29
+ identifier_snapshot = list(identifiers)
30
+
31
+ def _select(context: KompasContext) -> ShowComponentsResult:
32
+ return select_components_and_zoom(context.modules, context.app, identifier_snapshot)
33
+
34
+ return await session.run(
35
+ operation_name="assembly_navigator.show_component",
36
+ operation=_select,
37
+ )
@@ -0,0 +1,5 @@
1
+ """Raw KsAPI reads for the assembly navigator: reader, resolver, presentation, mapping.
2
+
3
+ Everything that touches raw `IPart`/`ksapi.py` objects lives here, inside a
4
+ `KompasSession.run(...)` callback — no raw handle leaves this package.
5
+ """
@@ -0,0 +1,122 @@
1
+ """Pure RawAssembly -> AssemblyView mapping without live KsAPI calls.
2
+
3
+ Перенесено из `bridge/kompas_ksapi/pure/assembly.py` (E16.04) без изменения поведения.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from kompas_kernel.features.assembly_navigator.ksapi.raw import (
9
+ RawAssembly,
10
+ RawComponent,
11
+ RawStandardComponentInfo,
12
+ RawStandardReferenceObject,
13
+ )
14
+ from kompas_kernel.features.assembly_navigator.models import (
15
+ AssemblyComponent,
16
+ AssemblyView,
17
+ ComponentKind,
18
+ ComponentLoadState,
19
+ ComponentPlacement,
20
+ StandardComponentAxis,
21
+ StandardComponentInfo,
22
+ StandardComponentPlane,
23
+ StandardReferenceObject,
24
+ )
25
+ from kompas_kernel.kompas.models import BBox, Point3D
26
+ from kompas_kernel.kompas.objects import Gabarit
27
+
28
+ # Маппинг ksLoadStateEnum (ksConstants.h) -> ComponentLoadState.
29
+ # Кодировка: -1 = недоступно/неизвестно, 0 = полная загрузка, 1 = выгружен,
30
+ # 2 = только треугольники, 3 = частичная загрузка, 4 = только габарит.
31
+ # Неизвестный код (не перечислен ниже) -> UNKNOWN.
32
+ _LOAD_STATE_MAP: dict[int, ComponentLoadState] = {
33
+ -1: ComponentLoadState.UNKNOWN,
34
+ 0: ComponentLoadState.FULL,
35
+ 1: ComponentLoadState.UNLOADED,
36
+ 2: ComponentLoadState.TRIANGLES,
37
+ 3: ComponentLoadState.PARTIAL,
38
+ 4: ComponentLoadState.GABARIT,
39
+ }
40
+
41
+
42
+ def build_assembly_view(raw: RawAssembly) -> AssemblyView:
43
+ """`RawAssembly` -> `AssemblyView`: маппит сырые поля в DTO без КОМПАС.
44
+
45
+ Не делает запросов к KsAPI - полностью детерминирован и тестируем без КОМПАС.
46
+ """
47
+ components = [_build_component(rc) for rc in raw.components]
48
+ return AssemblyView(
49
+ is_assembly=raw.is_assembly,
50
+ top_marking=raw.top_marking,
51
+ components=components,
52
+ component_count=len(components),
53
+ )
54
+
55
+
56
+ def _build_component(rc: RawComponent) -> AssemblyComponent:
57
+ """Маппинг одного RawComponent -> AssemblyComponent (порт DTO)."""
58
+ bbox = None if rc.gabarit_mm is None else _bbox_from_gabarit(rc.gabarit_mm)
59
+ load_state = _LOAD_STATE_MAP.get(rc.load_state_code, ComponentLoadState.UNKNOWN)
60
+ kind = ComponentKind.PART if rc.is_detail else ComponentKind.SUBASSEMBLY
61
+ placement = ComponentPlacement(
62
+ origin_x=rc.origin_x,
63
+ origin_y=rc.origin_y,
64
+ origin_z=rc.origin_z,
65
+ )
66
+ return AssemblyComponent(
67
+ identifier=rc.identifier,
68
+ marking=rc.marking,
69
+ name=rc.name,
70
+ kind=kind,
71
+ is_standard=rc.is_standard,
72
+ placement=placement,
73
+ bbox=bbox,
74
+ standard=_build_standard_info(rc.standard),
75
+ load_state=load_state,
76
+ visible=rc.visible,
77
+ valid=rc.valid,
78
+ excluded=rc.excluded,
79
+ )
80
+
81
+
82
+ def _build_standard_info(raw: RawStandardComponentInfo | None) -> StandardComponentInfo | None:
83
+ if raw is None:
84
+ return None
85
+ axis = None
86
+ if raw.axis_origin is not None and raw.axis_direction is not None:
87
+ axis = StandardComponentAxis(
88
+ origin=_point_from_tuple(raw.axis_origin),
89
+ direction=_point_from_tuple(raw.axis_direction),
90
+ reference=_build_standard_reference(raw.axis_reference),
91
+ )
92
+ plane = None
93
+ if raw.plane_origin is not None and raw.plane_normal is not None:
94
+ plane = StandardComponentPlane(
95
+ origin=_point_from_tuple(raw.plane_origin),
96
+ normal=_point_from_tuple(raw.plane_normal),
97
+ reference=_build_standard_reference(raw.plane_reference),
98
+ )
99
+ return StandardComponentInfo(file_name=raw.file_name, axis=axis, plane=plane)
100
+
101
+
102
+ def _build_standard_reference(raw: RawStandardReferenceObject | None) -> StandardReferenceObject | None:
103
+ if raw is None:
104
+ return None
105
+ return StandardReferenceObject(name=raw.name, model_object_type=raw.model_object_type, source=raw.source)
106
+
107
+
108
+ def _point_from_tuple(point: tuple[float, float, float]) -> Point3D:
109
+ return Point3D(x_mm=point[0], y_mm=point[1], z_mm=point[2])
110
+
111
+
112
+ def _bbox_from_gabarit(gabarit: Gabarit) -> BBox:
113
+ """Кортеж (x1, y1, z1, x2, y2, z2) -> BBox с нормализованными min/max."""
114
+ x1, y1, z1, x2, y2, z2 = gabarit
115
+ return BBox(
116
+ min_x=min(x1, x2),
117
+ min_y=min(y1, y2),
118
+ min_z=min(z1, z2),
119
+ max_x=max(x1, x2),
120
+ max_y=max(y1, y2),
121
+ max_z=max(z1, z2),
122
+ )
@@ -0,0 +1,107 @@
1
+ """Live KsAPI select+zoom for one or more assembly components.
2
+
3
+ Перенесено дословно из `KompasKsApiAdapter._select_components_and_zoom`
4
+ (`bridge/kompas_ksapi/adapter.py:497-578`, E16.04): один `Select` на все найденные цели,
5
+ один `ksCMZoomSelected` c `async=True`, тот же лог-эвент
6
+ `bridge.ksapi_show_component.completed`. Индекс резолвится тем же `resolver.py`, что и
7
+ `reader.py` — устраняет прежнее раздвоение numbering/parsing между чтением и показом.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from kompas_kernel.features.assembly_navigator.ksapi.resolver import list_components, resolve_index
15
+ from kompas_kernel.features.assembly_navigator.models import ComponentShowResult, ShowComponentsResult
16
+ from kompas_kernel.kompas.errors import KompasError
17
+ from kompas_kernel.kompas.objects import as_document_3d, constants_class
18
+ from kompas_kernel.kompas.runtime import KsApiModules
19
+ from kompas_kernel.observability.logger import get_logger
20
+ from kompas_kernel.observability.taxonomy import LogDomain, LogEventType, event_name
21
+
22
+ logger = get_logger(__name__)
23
+
24
+
25
+ def select_components_and_zoom(modules: KsApiModules, app: Any, identifiers: list[str]) -> ShowComponentsResult:
26
+ """Выделить найденные компоненты одним `Select`, затем зумнуть одной командой.
27
+
28
+ Невалидный индекс отражается как `found=False` у соответствующего результата и не
29
+ мешает показать остальные найденные компоненты.
30
+ """
31
+ doc3d = as_document_3d(modules, app.GetActiveDocument())
32
+ if doc3d is None:
33
+ raise KompasError("show_component: активный документ не 3D")
34
+ constants = constants_class(modules)
35
+ top = doc3d.GetTopPart()
36
+ components = list_components(modules, top)
37
+
38
+ resolutions: list[tuple[str, int | None]] = []
39
+ target_indices: list[int] = []
40
+ seen_indices: set[int] = set()
41
+ for requested_identifier in identifiers:
42
+ resolved_index = resolve_index(components, requested_identifier)
43
+ resolutions.append((requested_identifier, resolved_index))
44
+ if resolved_index is not None and resolved_index not in seen_indices:
45
+ seen_indices.add(resolved_index)
46
+ target_indices.append(resolved_index)
47
+
48
+ if not target_indices:
49
+ logger.info(
50
+ event_name(LogDomain.BRIDGE, "ksapi_show_component", LogEventType.COMPLETED),
51
+ identifiers=identifiers,
52
+ requested_count=len(identifiers),
53
+ found_count=0,
54
+ selected_count=0,
55
+ zoomed=False,
56
+ )
57
+ return ShowComponentsResult(
58
+ components=tuple(
59
+ ComponentShowResult(identifier=value, found=False, selected=False, marking="")
60
+ for value, _index in resolutions
61
+ ),
62
+ zoomed=False,
63
+ )
64
+
65
+ targets = [components[index] for index in target_indices]
66
+ sm = doc3d.GetSelectionManager()
67
+ sm.UnselectAll()
68
+ sm.Select(targets)
69
+ selected_by_index = {
70
+ index: bool(sm.IsSelected(target)) for index, target in zip(target_indices, targets, strict=True)
71
+ }
72
+ all_found_selected = all(selected_by_index.values())
73
+ # Зум по выделению: ExecuteKompasCommand требует async=True — sync с worker-потока
74
+ # не отрабатывает (ksCMZoomSelected = 32416). См. docs/recipes/ksapi-link.md.
75
+ zoomed = all_found_selected and bool(app.ExecuteKompasCommand(int(constants.ksCMZoomSelected), True))
76
+
77
+ results: list[ComponentShowResult] = []
78
+ for requested_identifier, index in resolutions:
79
+ if index is None:
80
+ results.append(
81
+ ComponentShowResult(
82
+ identifier=requested_identifier,
83
+ found=False,
84
+ selected=False,
85
+ marking="",
86
+ )
87
+ )
88
+ continue
89
+ marking_raw = components[index].GetMarking()
90
+ results.append(
91
+ ComponentShowResult(
92
+ identifier=requested_identifier,
93
+ found=True,
94
+ selected=selected_by_index[index],
95
+ marking=str(marking_raw) if marking_raw is not None else "",
96
+ )
97
+ )
98
+
99
+ logger.info(
100
+ event_name(LogDomain.BRIDGE, "ksapi_show_component", LogEventType.COMPLETED),
101
+ identifiers=identifiers,
102
+ requested_count=len(identifiers),
103
+ found_count=sum(index is not None for _identifier, index in resolutions),
104
+ selected_count=sum(selected_by_index[index] for _identifier, index in resolutions if index is not None),
105
+ zoomed=zoomed,
106
+ )
107
+ return ShowComponentsResult(components=tuple(results), zoomed=zoomed)
@@ -0,0 +1,74 @@
1
+ """Raw assembly facts read from KsAPI before mapping to public DTOs.
2
+
3
+ Перенесено из `bridge/kompas_ksapi/raw/assembly.py` (E16.04). `Gabarit` теперь живёт в
4
+ `kompas_kernel.kompas.objects` (общий примитив, используется и вне assembly navigator).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+
11
+ from kompas_kernel.features.assembly_navigator.models import StandardReferenceSource
12
+ from kompas_kernel.kompas.objects import Gabarit
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class RawStandardReferenceObject:
17
+ """Сырой факт о найденном опорном объекте стандартного компонента."""
18
+
19
+ name: str
20
+ model_object_type: int
21
+ source: StandardReferenceSource
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class RawStandardComponentInfo:
26
+ """Сырые дополнительные поля для `IPart.IsStandard()==True`."""
27
+
28
+ file_name: str | None
29
+ axis_origin: tuple[float, float, float] | None
30
+ axis_direction: tuple[float, float, float] | None
31
+ axis_reference: RawStandardReferenceObject | None
32
+ plane_origin: tuple[float, float, float] | None
33
+ plane_normal: tuple[float, float, float] | None
34
+ plane_reference: RawStandardReferenceObject | None
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class RawComponent:
39
+ """Сырые поля одного компонента (IPart) из GetPartsArray - до маппинга в DTO.
40
+
41
+ `identifier` - строковый индекс в массиве GetPartsArray, стабилен в сессии.
42
+ `name` - None, если GetName() вернул пустую строку (нормализуется reader'ом).
43
+ `gabarit_mm` - (x1, y1, z1, x2, y2, z2) в мм; None если нет тела/габарита.
44
+ `load_state_code` - сырое целое из GetLoadState() (ksLoadStateEnum).
45
+ """
46
+
47
+ identifier: str
48
+ marking: str
49
+ name: str | None
50
+ is_standard: bool
51
+ standard: RawStandardComponentInfo | None
52
+ is_detail: bool
53
+ origin_x: float
54
+ origin_y: float
55
+ origin_z: float
56
+ gabarit_mm: Gabarit | None
57
+ load_state_code: int
58
+ visible: bool
59
+ valid: bool
60
+ excluded: bool
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class RawAssembly:
65
+ """Сырой снимок верхнего уровня сборки/детали - до маппинга в AssemblyView.
66
+
67
+ `is_assembly` - True если GetDocumentType() == ksDocumentAssembly (5).
68
+ `top_marking` - обозначение головного Part из GetMarking().
69
+ `components` - список сырых компонентов верхнего уровня (все вставки, дубли сохранены).
70
+ """
71
+
72
+ is_assembly: bool
73
+ top_marking: str
74
+ components: list[RawComponent]
@@ -0,0 +1,287 @@
1
+ """Live KsAPI reader for top-level assembly components and standard-component facts.
2
+
3
+ Перенесено из `bridge/kompas_ksapi/api/assembly_reader.py` + `api/standard_parts.py`
4
+ (E16.04, слиты в один файл — `standard_parts.py` был единственным потребителем
5
+ `read_raw_standard_component`). Компонентная нумерация и резолв индекса теперь общие
6
+ с `presentation.py` через `resolver.py` (устраняет прежнее раздвоение `enumerate` vs
7
+ `_parse_index`), поведение и семантика не изменились.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Callable
13
+ from typing import Any
14
+
15
+ from kompas_kernel.features.assembly_navigator.ksapi.raw import (
16
+ RawAssembly,
17
+ RawComponent,
18
+ RawStandardComponentInfo,
19
+ RawStandardReferenceObject,
20
+ )
21
+ from kompas_kernel.features.assembly_navigator.ksapi.resolver import component_identifier, list_components
22
+ from kompas_kernel.features.assembly_navigator.models import StandardReferenceSource
23
+ from kompas_kernel.kompas.errors import KompasError
24
+ from kompas_kernel.kompas.objects import as_document_3d, constants3d_class, constants_class, part_gabarit
25
+ from kompas_kernel.kompas.runtime import KsApiModules
26
+ from kompas_kernel.observability.logger import get_logger
27
+ from kompas_kernel.observability.taxonomy import LogDomain, LogEventType, event_name
28
+
29
+ logger = get_logger(__name__)
30
+
31
+ _GET_VECTOR_VALUE_COUNT = 4
32
+ _STANDARD_AXIS_NAMES = ("Axis",)
33
+ _STANDARD_PLANE_NAMES = ("Plane", "Plain")
34
+ _UNNAMED_REFERENCE_NAME = "<unnamed>"
35
+
36
+
37
+ def read_raw_assembly(modules: KsApiModules, app: Any) -> RawAssembly:
38
+ doc = app.GetActiveDocument()
39
+ doc3d = as_document_3d(modules, doc)
40
+ if doc3d is None:
41
+ raise KompasError("read_assembly: активный документ не 3D")
42
+ constants = constants_class(modules)
43
+ constants3d = constants3d_class(modules)
44
+ is_assembly: bool = bool(doc.GetDocumentType() == constants.ksDocumentAssembly)
45
+ top = doc3d.GetTopPart()
46
+ parts_list = list_components(modules, top)
47
+ components = [_read_raw_component(constants3d=constants3d, part=p, index=i) for i, p in enumerate(parts_list)]
48
+ top_marking_raw = top.GetMarking()
49
+ top_marking = str(top_marking_raw) if top_marking_raw is not None else ""
50
+ return RawAssembly(
51
+ is_assembly=is_assembly,
52
+ top_marking=top_marking,
53
+ components=components,
54
+ )
55
+
56
+
57
+ def _read_raw_component(*, constants3d: Any, part: Any, index: int) -> RawComponent:
58
+ """Прочитать поля одного компонента (IPart) с worker-потока -> RawComponent."""
59
+ identifier = component_identifier(index)
60
+ marking = str(part.GetMarking() or "")
61
+ raw_name = str(part.GetName() or "")
62
+ name: str | None = raw_name or None
63
+ is_standard = bool(part.IsStandard())
64
+ is_detail = bool(part.IsDetail())
65
+
66
+ ox: float = 0.0
67
+ oy: float = 0.0
68
+ oz: float = 0.0
69
+ axis_direction: tuple[float, float, float] | None = None
70
+ try:
71
+ pl = part.GetPlacement()
72
+ # GetOrigin - out-параметры: возвращает (ok, x, y, z), а не голую тройку.
73
+ origin = pl.GetOrigin(0.0, 0.0, 0.0)
74
+ ox = float(origin[1])
75
+ oy = float(origin[2])
76
+ oz = float(origin[3])
77
+ # GetMatrix3D() (16 float, 4x4) не читаем: её не потреблял никто, кроме выдачи
78
+ # модели, а там она стоила ~450 байт на компонент. Ориентация стандартного
79
+ # изделия берётся ниже из GetVector(o3d_axisOZ) - точнее и дешевле.
80
+ except Exception as exc:
81
+ logger.warning(
82
+ event_name(LogDomain.BRIDGE, "ksapi_read_component_placement", LogEventType.FAILED),
83
+ identifier=identifier,
84
+ error=str(exc),
85
+ )
86
+ else:
87
+ axis_direction = _placement_axis_direction(
88
+ placement=pl,
89
+ constants3d=constants3d,
90
+ identifier=identifier,
91
+ )
92
+
93
+ gabarit_mm = part_gabarit(part, body_count=1)
94
+ load_state_code = int(part.GetLoadState())
95
+ visible = bool(part.IsStaffVisible())
96
+ valid = bool(part.IsValid())
97
+ excluded = bool(part.IsInheritExclude())
98
+ standard = (
99
+ read_raw_standard_component(
100
+ constants3d=constants3d,
101
+ part=part,
102
+ identifier=identifier,
103
+ origin=(ox, oy, oz),
104
+ axis_direction=axis_direction,
105
+ )
106
+ if is_standard
107
+ else None
108
+ )
109
+
110
+ return RawComponent(
111
+ identifier=identifier,
112
+ marking=marking,
113
+ name=name,
114
+ is_standard=is_standard,
115
+ standard=standard,
116
+ is_detail=is_detail,
117
+ origin_x=ox,
118
+ origin_y=oy,
119
+ origin_z=oz,
120
+ gabarit_mm=gabarit_mm,
121
+ load_state_code=load_state_code,
122
+ visible=visible,
123
+ valid=valid,
124
+ excluded=excluded,
125
+ )
126
+
127
+
128
+ def _placement_axis_direction(
129
+ *,
130
+ placement: Any,
131
+ constants3d: Any,
132
+ identifier: str,
133
+ ) -> tuple[float, float, float] | None:
134
+ try:
135
+ vector = placement.GetVector(int(constants3d.o3d_axisOZ), 0.0, 0.0, 0.0)
136
+ except Exception as exc:
137
+ logger.warning(
138
+ event_name(LogDomain.BRIDGE, "ksapi_read_component_axis", LogEventType.FAILED),
139
+ identifier=identifier,
140
+ error=str(exc),
141
+ )
142
+ return None
143
+ if vector is None:
144
+ return None
145
+ try:
146
+ values = tuple(vector)
147
+ except TypeError:
148
+ logger.warning(
149
+ event_name(LogDomain.BRIDGE, "ksapi_read_component_axis", LogEventType.FAILED),
150
+ identifier=identifier,
151
+ error=f"unexpected GetVector result: {vector!r}",
152
+ )
153
+ return None
154
+ if len(values) < _GET_VECTOR_VALUE_COUNT or not bool(values[0]):
155
+ return None
156
+ return (float(values[1]), float(values[2]), float(values[3]))
157
+
158
+
159
+ def read_raw_standard_component(
160
+ *,
161
+ constants3d: Any,
162
+ part: Any,
163
+ identifier: str,
164
+ origin: tuple[float, float, float],
165
+ axis_direction: tuple[float, float, float] | None,
166
+ ) -> RawStandardComponentInfo:
167
+ file_name = _read_optional_string(
168
+ part.GetFileName,
169
+ identifier=identifier,
170
+ field="file_name",
171
+ )
172
+ axis_reference = _read_standard_reference(
173
+ constants3d=constants3d,
174
+ part=part,
175
+ identifier=identifier,
176
+ role="axis",
177
+ named_candidates=_STANDARD_AXIS_NAMES,
178
+ default_object_type=int(constants3d.o3d_axisOZ),
179
+ )
180
+ plane_reference = _read_standard_reference(
181
+ constants3d=constants3d,
182
+ part=part,
183
+ identifier=identifier,
184
+ role="plane",
185
+ named_candidates=_STANDARD_PLANE_NAMES,
186
+ default_object_type=int(constants3d.o3d_planeXOY),
187
+ )
188
+ return RawStandardComponentInfo(
189
+ file_name=file_name,
190
+ axis_origin=origin if axis_direction is not None else None,
191
+ axis_direction=axis_direction,
192
+ axis_reference=axis_reference,
193
+ plane_origin=origin if axis_direction is not None else None,
194
+ plane_normal=axis_direction,
195
+ plane_reference=plane_reference,
196
+ )
197
+
198
+
199
+ def _read_optional_string(read: Callable[[], object], *, identifier: str, field: str) -> str | None:
200
+ try:
201
+ raw = read()
202
+ except Exception as exc:
203
+ logger.warning(
204
+ event_name(LogDomain.BRIDGE, "ksapi_read_component_field", LogEventType.FAILED),
205
+ identifier=identifier,
206
+ field=field,
207
+ error=str(exc),
208
+ )
209
+ return None
210
+ value = str(raw or "")
211
+ return value or None
212
+
213
+
214
+ def _read_standard_reference(
215
+ *,
216
+ constants3d: Any,
217
+ part: Any,
218
+ identifier: str,
219
+ role: str,
220
+ named_candidates: tuple[str, ...],
221
+ default_object_type: int,
222
+ ) -> RawStandardReferenceObject | None:
223
+ unknown_type = int(constants3d.o3d_unknown)
224
+ for name in named_candidates:
225
+ try:
226
+ obj = part.GetObjectByName(name, unknown_type, True, True)
227
+ except Exception as exc:
228
+ logger.warning(
229
+ event_name(LogDomain.BRIDGE, "ksapi_read_standard_reference", LogEventType.FAILED),
230
+ identifier=identifier,
231
+ role=role,
232
+ source=StandardReferenceSource.NAMED.value,
233
+ name=name,
234
+ error=str(exc),
235
+ )
236
+ continue
237
+ try:
238
+ reference = _standard_reference_from_object(obj, StandardReferenceSource.NAMED)
239
+ except Exception as exc:
240
+ logger.warning(
241
+ event_name(LogDomain.BRIDGE, "ksapi_read_standard_reference", LogEventType.FAILED),
242
+ identifier=identifier,
243
+ role=role,
244
+ source=StandardReferenceSource.NAMED.value,
245
+ name=name,
246
+ error=str(exc),
247
+ )
248
+ continue
249
+ if reference is not None:
250
+ return reference
251
+ try:
252
+ obj = part.GetDefaultObject(default_object_type)
253
+ except Exception as exc:
254
+ logger.warning(
255
+ event_name(LogDomain.BRIDGE, "ksapi_read_standard_reference", LogEventType.FAILED),
256
+ identifier=identifier,
257
+ role=role,
258
+ source=StandardReferenceSource.DEFAULT.value,
259
+ error=str(exc),
260
+ )
261
+ return None
262
+ try:
263
+ return _standard_reference_from_object(obj, StandardReferenceSource.DEFAULT)
264
+ except Exception as exc:
265
+ logger.warning(
266
+ event_name(LogDomain.BRIDGE, "ksapi_read_standard_reference", LogEventType.FAILED),
267
+ identifier=identifier,
268
+ role=role,
269
+ source=StandardReferenceSource.DEFAULT.value,
270
+ error=str(exc),
271
+ )
272
+ return None
273
+
274
+
275
+ def _standard_reference_from_object(
276
+ obj: Any | None,
277
+ source: StandardReferenceSource,
278
+ ) -> RawStandardReferenceObject | None:
279
+ if obj is None:
280
+ return None
281
+ raw_name = str(obj.GetName() or "")
282
+ name = raw_name or _UNNAMED_REFERENCE_NAME
283
+ return RawStandardReferenceObject(
284
+ name=name,
285
+ model_object_type=int(obj.GetModelObjectType()),
286
+ source=source,
287
+ )