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,94 @@
|
|
|
1
|
+
"""Framework-free public document-state commands for document session, over `KompasSession`.
|
|
2
|
+
|
|
3
|
+
No Pydantic AI, no `kompas_copilot.bridge` import — raw checkpoint/restore KsAPI calls
|
|
4
|
+
live in `ksapi/snapshot.py`; this module wires them through session callbacks and owns
|
|
5
|
+
the digest-before/after honesty check shared by both operations.
|
|
6
|
+
|
|
7
|
+
CQS: both commands here mutate DOCUMENT state (`checkpoint` saves the active document
|
|
8
|
+
to disk, `restore` closes/overwrites/reopens it) — unlike `queries.screenshot_window`,
|
|
9
|
+
which only reads the current view. Neither knows about the session workspace or the
|
|
10
|
+
checkpoint registry; that's `service.CheckpointService`'s job (multi-step orchestration).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from uuid import uuid4
|
|
18
|
+
|
|
19
|
+
from kompas_kernel.features.document_session.digest import build_digest, digest_changed
|
|
20
|
+
from kompas_kernel.features.document_session.ksapi import context_reader, snapshot
|
|
21
|
+
from kompas_kernel.features.document_session.models import Checkpoint, GeoDigest, RestoreResult
|
|
22
|
+
from kompas_kernel.kompas.session import KompasContext, KompasSession
|
|
23
|
+
from kompas_kernel.observability.logger import get_logger
|
|
24
|
+
from kompas_kernel.observability.taxonomy import LogDomain, LogEventType, event_name
|
|
25
|
+
|
|
26
|
+
logger = get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def checkpoint(session: KompasSession, *, checkpoint_dir: Path, untitled_document_dir: Path) -> Checkpoint:
|
|
30
|
+
"""Снять снимок активного 3D-документа; безымянный сперва сохранить в рабочую область.
|
|
31
|
+
|
|
32
|
+
Оба каталога приходят снаружи (`service.CheckpointService` резолвит их от корня
|
|
33
|
+
сессии) — этот модуль не знает ни про рабочую область, ни про `session_id`.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def _operation(context: KompasContext) -> Checkpoint:
|
|
37
|
+
checkpoint_id = uuid4().hex
|
|
38
|
+
digest = build_digest(context_reader.read_active_topology(context.modules, context.app))
|
|
39
|
+
saved = snapshot.save_snapshot(
|
|
40
|
+
context.modules,
|
|
41
|
+
context.app,
|
|
42
|
+
checkpoint_dir=checkpoint_dir,
|
|
43
|
+
checkpoint_id=checkpoint_id,
|
|
44
|
+
untitled_document_dir=untitled_document_dir,
|
|
45
|
+
)
|
|
46
|
+
logger.info(
|
|
47
|
+
event_name(LogDomain.BRIDGE, "ksapi_checkpoint", LogEventType.COMPLETED),
|
|
48
|
+
checkpoint_id=checkpoint_id,
|
|
49
|
+
path=str(saved.checkpoint_path),
|
|
50
|
+
saved_untitled_document=saved.saved_untitled_document,
|
|
51
|
+
)
|
|
52
|
+
return Checkpoint(
|
|
53
|
+
checkpoint_id=checkpoint_id,
|
|
54
|
+
path=saved.checkpoint_path,
|
|
55
|
+
document_path=saved.document_path,
|
|
56
|
+
digest=digest,
|
|
57
|
+
created_at=datetime.now(UTC),
|
|
58
|
+
saved_untitled_document=saved.saved_untitled_document,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return await session.run(operation_name="document_session.checkpoint", operation=_operation)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def restore(session: KompasSession, target: Checkpoint) -> RestoreResult:
|
|
65
|
+
"""Откатить документ к переданному снимку — дайджест снимка берётся из `target`."""
|
|
66
|
+
|
|
67
|
+
def _operation(context: KompasContext) -> RestoreResult:
|
|
68
|
+
digest_before: GeoDigest | None
|
|
69
|
+
try:
|
|
70
|
+
digest_before = build_digest(context_reader.read_active_topology(context.modules, context.app))
|
|
71
|
+
except Exception as exc: # KsAPI can refuse reads precisely when rollback is required.
|
|
72
|
+
digest_before = None
|
|
73
|
+
logger.warning(
|
|
74
|
+
event_name(LogDomain.BRIDGE, "ksapi_restore_pre_digest", LogEventType.FAILED),
|
|
75
|
+
checkpoint_id=target.checkpoint_id,
|
|
76
|
+
error=str(exc),
|
|
77
|
+
)
|
|
78
|
+
snapshot.restore_snapshot(context.modules, context.app, target)
|
|
79
|
+
digest_after = build_digest(context_reader.read_active_topology(context.modules, context.app))
|
|
80
|
+
logger.info(
|
|
81
|
+
event_name(LogDomain.BRIDGE, "ksapi_restore", LogEventType.COMPLETED),
|
|
82
|
+
checkpoint_id=target.checkpoint_id,
|
|
83
|
+
document_path=target.document_path,
|
|
84
|
+
)
|
|
85
|
+
return RestoreResult(
|
|
86
|
+
ok=True,
|
|
87
|
+
restored_from=target.path,
|
|
88
|
+
document_path=target.document_path,
|
|
89
|
+
digest_before=digest_before,
|
|
90
|
+
digest_after=digest_after,
|
|
91
|
+
matches_checkpoint=not digest_changed(target.digest, digest_after),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return await session.run(operation_name="document_session.restore", operation=_operation)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Сборка геометрического дайджеста — чистое ядро, отделённое от CAD runtime.
|
|
2
|
+
|
|
3
|
+
Здесь нет KsAPI runtime-объектов: чтение сырой топологии живёт в `ksapi/context_reader.py`,
|
|
4
|
+
а сюда приходит уже через структурный `TopologyReader` или готовый `RawTopology`. Так
|
|
5
|
+
нормализация единиц, форма дайджеста и логика "изменилось/нет" тестируются фейком.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Protocol
|
|
12
|
+
|
|
13
|
+
from kompas_kernel.features.document_session.models import GeoDigest
|
|
14
|
+
from kompas_kernel.kompas.models import BBox
|
|
15
|
+
from kompas_kernel.kompas.objects import Gabarit
|
|
16
|
+
from kompas_kernel.kompas.units import SourceUnit, volume_to_mm3
|
|
17
|
+
|
|
18
|
+
# Порог совпадения непрерывных величин: ниже - считаем "не изменилось" (шум пересчёта).
|
|
19
|
+
_BBOX_TOL_MM = 1e-6
|
|
20
|
+
_VOLUME_TOL_MM3 = 1e-6
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class RawTopology:
|
|
25
|
+
"""Сырое чтение топологии активной 3D-детали - до нормализации в дайджест.
|
|
26
|
+
|
|
27
|
+
`gabarit_mm` уже в мм. `volume` несёт свою единицу `volume_unit` и нормализуется
|
|
28
|
+
в мм3 при сборке. `None` означает "нет данных".
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
body_count: int
|
|
32
|
+
face_count: int
|
|
33
|
+
edge_count: int
|
|
34
|
+
gabarit_mm: Gabarit | None
|
|
35
|
+
volume: float | None
|
|
36
|
+
volume_unit: SourceUnit
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TopologyReader(Protocol):
|
|
40
|
+
"""Структурный интерфейс чтения топологии.
|
|
41
|
+
|
|
42
|
+
Граница между "как читаем из CAD runtime" и "как собираем дайджест".
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def body_count(self) -> int: ...
|
|
46
|
+
|
|
47
|
+
def face_count(self) -> int: ...
|
|
48
|
+
|
|
49
|
+
def edge_count(self) -> int: ...
|
|
50
|
+
|
|
51
|
+
def gabarit_mm(self) -> Gabarit | None: ...
|
|
52
|
+
|
|
53
|
+
def volume(self) -> tuple[float, SourceUnit] | None: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def read_raw_topology(reader: TopologyReader) -> RawTopology:
|
|
57
|
+
"""Собрать `RawTopology`, дёрнув читателя. Command-свободно: только запросы (GPC3)."""
|
|
58
|
+
measured = reader.volume()
|
|
59
|
+
if measured is None:
|
|
60
|
+
volume, unit = None, SourceUnit.MM
|
|
61
|
+
else:
|
|
62
|
+
volume, unit = measured
|
|
63
|
+
return RawTopology(
|
|
64
|
+
body_count=reader.body_count(),
|
|
65
|
+
face_count=reader.face_count(),
|
|
66
|
+
edge_count=reader.edge_count(),
|
|
67
|
+
gabarit_mm=reader.gabarit_mm(),
|
|
68
|
+
volume=volume,
|
|
69
|
+
volume_unit=unit,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def build_digest(raw: RawTopology) -> GeoDigest:
|
|
74
|
+
"""`RawTopology` -> `GeoDigest`: нормализует объём в мм3 и разворачивает габарит в bbox."""
|
|
75
|
+
bbox = None if raw.gabarit_mm is None else _bbox_from_gabarit(raw.gabarit_mm)
|
|
76
|
+
volume_mm3 = None if raw.volume is None else volume_to_mm3(raw.volume, raw.volume_unit)
|
|
77
|
+
return GeoDigest(
|
|
78
|
+
body_count=raw.body_count,
|
|
79
|
+
face_count=raw.face_count,
|
|
80
|
+
edge_count=raw.edge_count,
|
|
81
|
+
volume_mm3=volume_mm3,
|
|
82
|
+
bbox=bbox,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _bbox_from_gabarit(gabarit: Gabarit) -> BBox:
|
|
87
|
+
x1, y1, z1, x2, y2, z2 = gabarit
|
|
88
|
+
return BBox(
|
|
89
|
+
min_x=min(x1, x2),
|
|
90
|
+
min_y=min(y1, y2),
|
|
91
|
+
min_z=min(z1, z2),
|
|
92
|
+
max_x=max(x1, x2),
|
|
93
|
+
max_y=max(y1, y2),
|
|
94
|
+
max_z=max(z1, z2),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def digest_changed(before: GeoDigest, after: GeoDigest) -> bool:
|
|
99
|
+
"""Изменилась ли геометрия по дайджесту - основа честности `run_python`/`restore`.
|
|
100
|
+
|
|
101
|
+
Считаем "изменилось", если различается хоть один структурный признак: число
|
|
102
|
+
тел/граней/рёбер (точно), габарит или объём (с порогом на шум пересчёта).
|
|
103
|
+
Появление/исчезновение габарита или объёма - тоже изменение.
|
|
104
|
+
"""
|
|
105
|
+
if (before.body_count, before.face_count, before.edge_count) != (
|
|
106
|
+
after.body_count,
|
|
107
|
+
after.face_count,
|
|
108
|
+
after.edge_count,
|
|
109
|
+
):
|
|
110
|
+
return True
|
|
111
|
+
if _bbox_changed(before.bbox, after.bbox):
|
|
112
|
+
return True
|
|
113
|
+
return _scalar_changed(before.volume_mm3, after.volume_mm3, _VOLUME_TOL_MM3)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _bbox_changed(before: BBox | None, after: BBox | None) -> bool:
|
|
117
|
+
if before is None or after is None:
|
|
118
|
+
return before is not after
|
|
119
|
+
corners = ("min_x", "min_y", "min_z", "max_x", "max_y", "max_z")
|
|
120
|
+
return any(abs(getattr(before, c) - getattr(after, c)) > _BBOX_TOL_MM for c in corners)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _scalar_changed(before: float | None, after: float | None, tol: float) -> bool:
|
|
124
|
+
if before is None or after is None:
|
|
125
|
+
return before is not after
|
|
126
|
+
return abs(before - after) > tol
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Raw KsAPI reads that back `document_session.queries` — context + topology.
|
|
2
|
+
|
|
3
|
+
Free functions taking explicit `modules`/`app` (no adapter `self` state), same facade
|
|
4
|
+
split as the other KsAPI capability modules (`kompas_kernel.kompas.objects`).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from kompas_kernel.features.document_session.digest import RawTopology, build_digest
|
|
12
|
+
from kompas_kernel.features.document_session.models import (
|
|
13
|
+
NO_ACTIVE_DOCUMENT,
|
|
14
|
+
ActiveDocument,
|
|
15
|
+
AssemblyContext,
|
|
16
|
+
CadContext,
|
|
17
|
+
DocumentContext,
|
|
18
|
+
DocumentKind,
|
|
19
|
+
PartContext,
|
|
20
|
+
simple_document_context,
|
|
21
|
+
)
|
|
22
|
+
from kompas_kernel.kompas.models import EntityKind, EntityRef
|
|
23
|
+
from kompas_kernel.kompas.objects import (
|
|
24
|
+
as_document_3d,
|
|
25
|
+
constants3d_class,
|
|
26
|
+
constants_class,
|
|
27
|
+
doc_name,
|
|
28
|
+
doc_path,
|
|
29
|
+
part_gabarit,
|
|
30
|
+
)
|
|
31
|
+
from kompas_kernel.kompas.runtime import KsApiModules
|
|
32
|
+
from kompas_kernel.kompas.units import SourceUnit
|
|
33
|
+
|
|
34
|
+
EMPTY_TOPOLOGY = RawTopology(
|
|
35
|
+
body_count=0,
|
|
36
|
+
face_count=0,
|
|
37
|
+
edge_count=0,
|
|
38
|
+
gabarit_mm=None,
|
|
39
|
+
volume=None,
|
|
40
|
+
volume_unit=SourceUnit.MM,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _active_part(modules: KsApiModules, app: Any) -> Any | None:
|
|
45
|
+
doc3d = as_document_3d(modules, app.GetActiveDocument())
|
|
46
|
+
return None if doc3d is None else doc3d.GetTopPart()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _document_kind_from_code(constants: Any, type_code: int) -> DocumentKind:
|
|
50
|
+
"""`ksDocumentTypeEnum` code → `DocumentKind`, через ИМЕНОВАННЫЕ константы (не магические int).
|
|
51
|
+
|
|
52
|
+
Незнакомый код → `UNKNOWN` (не падаем): его несёт `UnknownDocumentContext.raw_type_code`.
|
|
53
|
+
"""
|
|
54
|
+
mapping = {
|
|
55
|
+
int(constants.ksDocumentPart): DocumentKind.PART,
|
|
56
|
+
int(constants.ksDocumentAssembly): DocumentKind.ASSEMBLY,
|
|
57
|
+
int(constants.ksDocumentDrawing): DocumentKind.DRAWING,
|
|
58
|
+
int(constants.ksDocumentFragment): DocumentKind.FRAGMENT,
|
|
59
|
+
int(constants.ksDocumentSpecification): DocumentKind.SPECIFICATION,
|
|
60
|
+
int(constants.ksDocumentTextual): DocumentKind.TEXTUAL,
|
|
61
|
+
}
|
|
62
|
+
return mapping.get(type_code, DocumentKind.UNKNOWN)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _model_objects_count(container: Any, obj_type: int) -> int:
|
|
66
|
+
objects = container.GetObjects(obj_type)
|
|
67
|
+
return len(objects) if objects is not None else 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _count_bodies_via_faces(modules: KsApiModules, faces: list[Any]) -> int:
|
|
71
|
+
"""Число тел активной детали — через грани, НЕ через `GetObjects(o3d_body)`.
|
|
72
|
+
|
|
73
|
+
`GetObjects(o3d_body)` в КОМПАС не перечисляет тела (даёт 0 даже на детали с
|
|
74
|
+
телом). Берём тело каждой грани (`IFace.GetBody`) и дедуплицируем по
|
|
75
|
+
`IBody.GetBodyId`.
|
|
76
|
+
"""
|
|
77
|
+
body_ids: set[int] = set()
|
|
78
|
+
for face_obj in faces:
|
|
79
|
+
face = modules.ksapi.IFace(face_obj)
|
|
80
|
+
body = face.GetBody()
|
|
81
|
+
if body is None:
|
|
82
|
+
continue
|
|
83
|
+
body_ids.add(int(body.GetBodyId()))
|
|
84
|
+
return len(body_ids)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def read_active_topology(modules: KsApiModules, app: Any) -> RawTopology:
|
|
88
|
+
"""Сырая топология активной 3D-детали; нет активной детали → пустая топология."""
|
|
89
|
+
part = _active_part(modules, app)
|
|
90
|
+
if part is None:
|
|
91
|
+
return EMPTY_TOPOLOGY
|
|
92
|
+
return _read_part_topology(modules, part)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _read_part_topology(modules: KsApiModules, part: Any) -> RawTopology:
|
|
96
|
+
constants3d = constants3d_class(modules)
|
|
97
|
+
container = part.GetModelContainer()
|
|
98
|
+
if container is None:
|
|
99
|
+
return EMPTY_TOPOLOGY
|
|
100
|
+
raw_faces = container.GetObjects(int(constants3d.o3d_face))
|
|
101
|
+
faces: list[Any] = list(raw_faces) if raw_faces is not None else []
|
|
102
|
+
# Тела — через грани (o3d_body не перечисляется GetObjects, см. _count_bodies_via_faces).
|
|
103
|
+
body_count = _count_bodies_via_faces(modules, faces)
|
|
104
|
+
edge_count = _model_objects_count(container, int(constants3d.o3d_edge))
|
|
105
|
+
return RawTopology(
|
|
106
|
+
body_count=body_count,
|
|
107
|
+
face_count=len(faces),
|
|
108
|
+
edge_count=edge_count,
|
|
109
|
+
gabarit_mm=part_gabarit(part, body_count),
|
|
110
|
+
volume=None,
|
|
111
|
+
volume_unit=SourceUnit.MM,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def read_active_document(modules: KsApiModules, app: Any) -> ActiveDocument:
|
|
116
|
+
"""Кто открыт — четырьмя вызовами, без обращения к топологии (см. `ActiveDocument`).
|
|
117
|
+
|
|
118
|
+
Тип берётся из `GetDocumentType` тем же `_document_kind_from_code`, что и у полного
|
|
119
|
+
контекста: дискриминатор один, разъехаться они не могут. Ни `GetTopPart`, ни
|
|
120
|
+
`GetModelContainer`, ни `GetObjects` здесь не вызываются — в этом вся суть.
|
|
121
|
+
"""
|
|
122
|
+
doc = app.GetActiveDocument()
|
|
123
|
+
if doc is None:
|
|
124
|
+
return NO_ACTIVE_DOCUMENT
|
|
125
|
+
kind = _document_kind_from_code(constants_class(modules), int(doc.GetDocumentType()))
|
|
126
|
+
return ActiveDocument(document_open=True, doc_kind=kind, name=doc_name(doc), path=doc_path(doc))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def read_context(modules: KsApiModules, app: Any) -> CadContext:
|
|
130
|
+
"""Снимок текущего состояния сессии CAD для оркестратора (`get_context`)."""
|
|
131
|
+
version = str(app.GetApplicationName(True))
|
|
132
|
+
doc = app.GetActiveDocument()
|
|
133
|
+
if doc is None:
|
|
134
|
+
return CadContext(connected=True, app_version=version, document_open=False)
|
|
135
|
+
return CadContext(
|
|
136
|
+
connected=True,
|
|
137
|
+
app_version=version,
|
|
138
|
+
document_open=True,
|
|
139
|
+
document=EntityRef(kind=EntityKind.DOCUMENT, index=0, document_path=doc_path(doc)),
|
|
140
|
+
document_name=doc_name(doc),
|
|
141
|
+
document_context=_read_document_context(modules, app, doc),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _read_document_context(modules: KsApiModules, app: Any, doc: Any) -> DocumentContext:
|
|
146
|
+
"""Полиморфный контекст по типу документа сессии (`GetDocumentType`).
|
|
147
|
+
|
|
148
|
+
Деталь → digest (честный: свои тела). Сборка → лёгкая сводка. Прочее → payload-less
|
|
149
|
+
вариант. Дискриминируем по типу документа, а не по расширению исходника.
|
|
150
|
+
"""
|
|
151
|
+
constants = constants_class(modules)
|
|
152
|
+
type_code = int(doc.GetDocumentType())
|
|
153
|
+
kind = _document_kind_from_code(constants, type_code)
|
|
154
|
+
if kind is DocumentKind.PART:
|
|
155
|
+
part = _active_part(modules, app)
|
|
156
|
+
raw = EMPTY_TOPOLOGY if part is None else _read_part_topology(modules, part)
|
|
157
|
+
return PartContext(digest=build_digest(raw))
|
|
158
|
+
if kind is DocumentKind.ASSEMBLY:
|
|
159
|
+
return _read_assembly_context(modules, doc)
|
|
160
|
+
return simple_document_context(kind, type_code)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _read_assembly_context(modules: KsApiModules, doc: Any) -> AssemblyContext:
|
|
164
|
+
"""Лёгкая сводка сборки: число компонентов + обозначение корня, без чтения их геометрии."""
|
|
165
|
+
doc3d = as_document_3d(modules, doc)
|
|
166
|
+
if doc3d is None:
|
|
167
|
+
return AssemblyContext(component_count=0, top_marking=None)
|
|
168
|
+
constants3d = constants3d_class(modules)
|
|
169
|
+
top = doc3d.GetTopPart()
|
|
170
|
+
parts = top.GetPartsArray(int(constants3d.ksAllParts))
|
|
171
|
+
component_count = len(parts) if parts is not None else 0
|
|
172
|
+
raw_marking = top.GetMarking()
|
|
173
|
+
marking = str(raw_marking).strip() if raw_marking is not None else ""
|
|
174
|
+
return AssemblyContext(component_count=component_count, top_marking=marking or None)
|