feat: 完善 STEP 一级参数化编辑识别与关系式建模
This commit is contained in:
+631
-3
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
import math
|
||||
from pathlib import Path
|
||||
import time
|
||||
@@ -78,9 +79,12 @@ from OCC.Extend.TopologyUtils import TopologyExplorer, discretize_edge
|
||||
from .constants import CURVE_TYPES, FREEFORM_FACE_SURFACES, SNAPSHOT_FACE_LOGICAL_IDS_KEY, SURFACE_TYPES
|
||||
from .export import ExportMixin
|
||||
from .features import FeatureMixin
|
||||
from .asitus_bridge import run_asitus_hole_recognition
|
||||
from .operations import OperationMixin
|
||||
from .polydata import PolydataMixin
|
||||
from .recognition_graph import recognize_through_hole_regions
|
||||
from .recognition_priority import (
|
||||
external_relation_score_bonus,
|
||||
feature_recognition_priority,
|
||||
feature_recognition_priority_label,
|
||||
feature_recognition_priority_reason,
|
||||
@@ -119,6 +123,19 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_edge_ids_cache: dict[int, list[int]] = {}
|
||||
self._edge_face_ids_cache: dict[int, list[int]] = {}
|
||||
self._same_domain_face_ids_cache: dict[int, list[int]] = {}
|
||||
self._asitus_hole_regions_attempted = False
|
||||
self._asitus_hole_regions_loading = False
|
||||
self._asitus_hole_region_cache: dict[int, list[int]] = {}
|
||||
self._asitus_hole_recognition_info: dict[str, object] = {}
|
||||
self._asitus_face_relation_cache: dict[int, dict[str, object]] = {}
|
||||
self._asitus_adjacency_relation_cache: dict[tuple[int, int], dict[str, object]] = {}
|
||||
self._asitus_geometric_relation_cache: dict[tuple[int, int], list[dict[str, object]]] = {}
|
||||
self._internal_hole_regions_attempted = False
|
||||
self._internal_hole_region_cache: dict[int, list[int]] = {}
|
||||
self._internal_recognition_info: dict[str, object] = {}
|
||||
self._recognition_graph_cache: dict[int, object] = {}
|
||||
self._through_hole_regions_cache: dict[tuple[object, ...], list[object]] = {}
|
||||
self._topology_refresh_generation = 0
|
||||
self._face_first_level_topology_cache: dict[int, dict[str, object]] = {}
|
||||
self._cylindrical_first_level_topology_cache: dict[int, dict[str, object]] = {}
|
||||
self._face_first_level_fact_cache: dict[tuple[int, str], dict[str, object]] = {}
|
||||
@@ -210,6 +227,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
return info
|
||||
|
||||
def refresh_topology(self) -> None:
|
||||
self._topology_refresh_generation = int(getattr(self, "_topology_refresh_generation", 0)) + 1
|
||||
self.shape = _compound_from_shapes([p.shape for p in self.display_parts()])
|
||||
self.faces.clear()
|
||||
self.face_logical_ids.clear()
|
||||
@@ -226,6 +244,18 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_edge_ids_cache.clear()
|
||||
self._edge_face_ids_cache.clear()
|
||||
self._same_domain_face_ids_cache.clear()
|
||||
self._asitus_hole_regions_attempted = False
|
||||
self._asitus_hole_regions_loading = False
|
||||
self._asitus_hole_region_cache.clear()
|
||||
self._asitus_hole_recognition_info.clear()
|
||||
self._asitus_face_relation_cache.clear()
|
||||
self._asitus_adjacency_relation_cache.clear()
|
||||
self._asitus_geometric_relation_cache.clear()
|
||||
self._internal_hole_regions_attempted = False
|
||||
self._internal_hole_region_cache.clear()
|
||||
self._internal_recognition_info.clear()
|
||||
self._recognition_graph_cache.clear()
|
||||
self._through_hole_regions_cache.clear()
|
||||
self._face_first_level_topology_cache.clear()
|
||||
self._cylindrical_first_level_topology_cache.clear()
|
||||
self._face_first_level_fact_cache.clear()
|
||||
@@ -359,8 +389,34 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
full_info = self._face_info_cache.get(face_id)
|
||||
if full_info is not None:
|
||||
info = dict(full_info)
|
||||
if str(info.get("surface") or "") == "cylinder" and not str(info.get("feature_type") or ""):
|
||||
info.update(self._cylindrical_feature_label_fields(info))
|
||||
info.update(self._asitus_face_summary_fields(face_id))
|
||||
if str(info.get("surface") or "") == "cylinder":
|
||||
try:
|
||||
hint = self._quick_cylindrical_feature_hint(
|
||||
face_id,
|
||||
BRepAdaptor_Surface(self.faces[face_id]),
|
||||
info,
|
||||
)
|
||||
for key in (
|
||||
"same_domain_face_ids",
|
||||
"same_domain_face_count",
|
||||
"feature_highlight_face_ids",
|
||||
"same_domain_angular_span",
|
||||
"same_domain_note",
|
||||
"same_domain_v_range",
|
||||
"same_domain_height_estimate",
|
||||
"same_domain_range_source",
|
||||
):
|
||||
if key in hint:
|
||||
info[key] = hint[key]
|
||||
if bool(hint.get("is_full_cylinder")):
|
||||
info["is_full_cylinder"] = True
|
||||
if hint.get("angular_span") is not None:
|
||||
info["angular_span"] = hint["angular_span"]
|
||||
except Exception:
|
||||
info.setdefault("feature_highlight_face_ids", (face_id,))
|
||||
if not str(info.get("feature_type") or ""):
|
||||
info.update(self._cylindrical_feature_label_fields(info))
|
||||
info.update(self._recognition_summary_fields(info))
|
||||
return info
|
||||
cached = self._quick_face_info_cache.get(face_id)
|
||||
@@ -514,6 +570,9 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
elif surface_label in FREEFORM_FACE_SURFACES:
|
||||
info.update(self._freeform_face_limit_fields(surface_label))
|
||||
|
||||
info.update(self._asitus_face_summary_fields(face_id))
|
||||
if surface_type == GeomAbs_Cylinder:
|
||||
info.update(self._asitus_cylindrical_feature_hint_fields(face_id, info))
|
||||
info.update(self._recognition_summary_fields(info))
|
||||
self._quick_face_info_cache[face_id] = dict(info)
|
||||
return dict(info)
|
||||
@@ -558,6 +617,125 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"feature_edit_actions": "可查看圆柱直径/半径;复杂语义需要手动扫描或执行计划确认。",
|
||||
}
|
||||
|
||||
def _asitus_cylindrical_feature_hint_fields(self, face_id: int, info: dict[str, object]) -> dict[str, object]:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
return {}
|
||||
if not self._asitus_adjacency_relation_cache and not self._asitus_geometric_relation_cache:
|
||||
return {}
|
||||
|
||||
angle_types: Counter[str] = Counter()
|
||||
relation_types: Counter[str] = Counter()
|
||||
related_face_ids: set[int] = set()
|
||||
for pair, relation in self._asitus_adjacency_relation_cache.items():
|
||||
if int(face_id) not in pair:
|
||||
continue
|
||||
related_face_ids.update(int(item) for item in pair if int(item) != int(face_id))
|
||||
angle_type = str(relation.get("angle_type") or "adjacent").strip().lower()
|
||||
if angle_type:
|
||||
angle_types[angle_type] += 1
|
||||
for pair, relations in self._asitus_geometric_relation_cache.items():
|
||||
if int(face_id) not in pair:
|
||||
continue
|
||||
related_face_ids.update(int(item) for item in pair if int(item) != int(face_id))
|
||||
for relation in relations:
|
||||
relation_type = str(relation.get("relation_type") or "").strip().lower()
|
||||
if relation_type:
|
||||
relation_types[relation_type] += 1
|
||||
if not angle_types and not relation_types:
|
||||
return {}
|
||||
|
||||
guess = str(info.get("feature_guess") or "")
|
||||
orientation = str(info.get("orientation") or "")
|
||||
angular_span = _float_or_none(info.get("same_domain_angular_span"))
|
||||
if angular_span is None:
|
||||
angular_span = _float_or_none(info.get("angular_span")) or 0.0
|
||||
is_full = bool(info.get("is_full_cylinder")) or angular_span >= math.tau * 0.92
|
||||
is_partial = 1.0e-6 < angular_span < math.tau * 0.92
|
||||
radius = _float_or_none(info.get("radius")) or 0.0
|
||||
boundary_edges = int(info.get("boundary_edges") or 0)
|
||||
solid_id = self.face_solid_ids[face_id] if 0 <= face_id < len(self.face_solid_ids) else -1
|
||||
solid_shape = self.solids[solid_id][1] if 0 <= solid_id < len(self.solids) else self.shape
|
||||
solid_diagonal = max(_shape_diagonal(solid_shape), 1.0)
|
||||
is_small_radius = radius > 0 and radius <= solid_diagonal * 0.04
|
||||
is_fillet_radius = radius > 0 and radius <= solid_diagonal * 0.12
|
||||
smooth_count = sum(value for key, value in angle_types.items() if "smooth" in key)
|
||||
convex_count = sum(value for key, value in angle_types.items() if "convex" in key)
|
||||
concave_count = sum(value for key, value in angle_types.items() if "concave" in key)
|
||||
tangent_count = int(relation_types.get("tangent", 0))
|
||||
coaxial_count = int(relation_types.get("coaxial", 0))
|
||||
parallel_axis_count = int(relation_types.get("parallel_axis", 0))
|
||||
planar_relation_count = sum(
|
||||
int(relation_types.get(key, 0))
|
||||
for key in ("coplanar", "parallel", "perpendicular")
|
||||
)
|
||||
|
||||
slot_score = 0
|
||||
if is_partial and guess == "hole/groove candidate":
|
||||
slot_score += 4
|
||||
slot_score += min((smooth_count + tangent_count) * 3, 6)
|
||||
slot_score += min(concave_count * 2, 4)
|
||||
slot_score += min(planar_relation_count, 3)
|
||||
|
||||
fillet_score = 0
|
||||
if is_partial and (
|
||||
guess == "round/fillet candidate"
|
||||
or (is_fillet_radius and guess not in {"hole/groove candidate", "boss/outer-round candidate"})
|
||||
):
|
||||
fillet_score += 3
|
||||
if is_small_radius:
|
||||
fillet_score += 4
|
||||
fillet_score += min((smooth_count + tangent_count) * 4, 8)
|
||||
if boundary_edges >= 4:
|
||||
fillet_score += 2
|
||||
|
||||
boss_score = 0
|
||||
is_boss_like = (
|
||||
guess == "boss/outer-round candidate"
|
||||
or (
|
||||
str(info.get("material_toward_axis") or "") == "inside"
|
||||
and "outside" in str(info.get("material_away_axis") or "")
|
||||
)
|
||||
or (is_full and orientation == "forward")
|
||||
)
|
||||
if is_full and is_boss_like:
|
||||
boss_score += 5
|
||||
boss_score += min(convex_count * 2, 4)
|
||||
boss_score += min(coaxial_count + parallel_axis_count, 4)
|
||||
boss_score += min(planar_relation_count, 3)
|
||||
|
||||
scores = {
|
||||
"slot": slot_score,
|
||||
"boss": boss_score,
|
||||
"fillet": fillet_score,
|
||||
}
|
||||
preferred, hint_score = max(scores.items(), key=lambda item: item[1])
|
||||
if hint_score <= 0:
|
||||
return {}
|
||||
|
||||
labels = {
|
||||
"slot": "槽 / 半孔",
|
||||
"boss": "凸台 / 外圆",
|
||||
"fillet": "圆角 / 倒圆",
|
||||
}
|
||||
relation_summary = ", ".join(f"{key}:{value}" for key, value in sorted(relation_types.items()))
|
||||
angle_summary = ", ".join(f"{key}:{value}" for key, value in sorted(angle_types.items()))
|
||||
summary_parts = [f"{labels[preferred]} +{hint_score}"]
|
||||
if relation_summary:
|
||||
summary_parts.append(f"几何关系 {relation_summary}")
|
||||
if angle_summary:
|
||||
summary_parts.append(f"AAG角度 {angle_summary}")
|
||||
return {
|
||||
"analysis_situs_feature_hint_status": "ready",
|
||||
"analysis_situs_feature_hint_preferred": preferred,
|
||||
"analysis_situs_feature_hint_label": labels[preferred],
|
||||
"analysis_situs_feature_hint_score": hint_score,
|
||||
"analysis_situs_feature_hint_summary": ";".join(summary_parts),
|
||||
"analysis_situs_feature_hint_related_face_ids": tuple(sorted(related_face_ids)),
|
||||
"analysis_situs_slot_hint_score": slot_score,
|
||||
"analysis_situs_boss_hint_score": boss_score,
|
||||
"analysis_situs_fillet_hint_score": fillet_score,
|
||||
}
|
||||
|
||||
def _quick_cylindrical_feature_hint(
|
||||
self,
|
||||
face_id: int,
|
||||
@@ -728,6 +906,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"v_range": (surf.FirstVParameter(), surf.LastVParameter()),
|
||||
"boundary_edges": boundary_edges,
|
||||
}
|
||||
info.update(self._asitus_face_summary_fields(face_id))
|
||||
info.update(_shape_bounds_info(face))
|
||||
info.update(self._face_boundary_wire_info(face))
|
||||
if surface_type == GeomAbs_Plane:
|
||||
@@ -807,6 +986,9 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
info["minor_radius"] = torus.MinorRadius()
|
||||
elif str(info.get("surface") or "") in FREEFORM_FACE_SURFACES:
|
||||
info.update(self._freeform_face_limit_fields(str(info.get("surface") or "")))
|
||||
info.update(self._asitus_face_summary_fields(face_id))
|
||||
if surface_type == GeomAbs_Cylinder:
|
||||
info.update(self._asitus_cylindrical_feature_hint_fields(face_id, info))
|
||||
info.update(self._recognition_summary_fields(info))
|
||||
self._face_info_cache[face_id] = dict(info)
|
||||
return dict(info)
|
||||
@@ -893,6 +1075,13 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
confidence = "high"
|
||||
if surface in FREEFORM_FACE_SURFACES:
|
||||
confidence = "low"
|
||||
external_relation_bonus = external_relation_score_bonus(info)
|
||||
if (
|
||||
external_relation_bonus >= 8
|
||||
and surface not in FREEFORM_FACE_SURFACES
|
||||
and confidence in {"low", "unchecked", "none"}
|
||||
):
|
||||
confidence = "medium"
|
||||
|
||||
risk_rank = {"low": 0, "medium": 1, "high": 2, "blocked": 3}
|
||||
capability_specs = (
|
||||
@@ -1024,6 +1213,21 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
add("first_level_topology", f"一级相邻Face={adjacent_count}")
|
||||
if info.get("first_level_fact_summary") not in {None, ""}:
|
||||
add("first_level_fact_graph", f"一级事实={info.get('first_level_fact_summary')}")
|
||||
if info.get("asitus_adjacent_face_count") not in {None, ""}:
|
||||
add(
|
||||
"analysis_situs_aag",
|
||||
f"Analysis Situs AAG adjacent Face={info.get('asitus_adjacent_face_count')}",
|
||||
)
|
||||
if info.get("asitus_geometric_relation_summary") not in {None, ""}:
|
||||
add(
|
||||
"analysis_situs_geometry",
|
||||
f"Analysis Situs geometry={info.get('asitus_geometric_relation_summary')}",
|
||||
)
|
||||
if info.get("analysis_situs_feature_hint_summary") not in {None, ""}:
|
||||
add(
|
||||
"analysis_situs_feature_hint",
|
||||
f"Analysis Situs hint={info.get('analysis_situs_feature_hint_summary')}",
|
||||
)
|
||||
if info.get("material_vote_summary") not in {None, ""}:
|
||||
add("material_votes", f"材料采样={info.get('material_vote_summary')}")
|
||||
if info.get("feature_end_face_ids") not in {None, ""}:
|
||||
@@ -1127,6 +1331,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
risk_penalty = {"low": 0, "medium": 14, "high": 30, "blocked": 72}
|
||||
score = confidence_points.get(confidence, 22)
|
||||
score += min(len(evidence_keys) * 5, 24)
|
||||
score += external_relation_bonus
|
||||
score -= risk_penalty.get(risk, 14)
|
||||
if blockers:
|
||||
score -= 35
|
||||
@@ -1183,6 +1388,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"recognition_user_priority_reason": user_priority_reason,
|
||||
"recognition_evidence": ";".join(evidence),
|
||||
"recognition_evidence_keys": tuple(evidence_keys),
|
||||
"recognition_external_relation_score_bonus": external_relation_bonus,
|
||||
"recognition_ready_actions": ";".join(ready_actions),
|
||||
"recognition_limited_actions": ";".join(limited_actions),
|
||||
"recognition_blockers": ";".join(blockers),
|
||||
@@ -2625,6 +2831,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
**fillet_info,
|
||||
}
|
||||
)
|
||||
result.update(self._asitus_cylindrical_feature_hint_fields(face_id, result))
|
||||
if angular_span >= math.tau * 0.92:
|
||||
result.update(
|
||||
{
|
||||
@@ -4423,7 +4630,11 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
if surface_type == GeomAbs_Plane:
|
||||
face_ids = self._connected_coplanar_planar_face_ids(face_id)
|
||||
elif surface_type == GeomAbs_Cylinder:
|
||||
face_ids = self._connected_cocylindrical_face_ids(face_id)
|
||||
face_ids = (
|
||||
self._asitus_hole_region_ids(face_id)
|
||||
or self._internal_hole_region_ids(face_id)
|
||||
or self._connected_cocylindrical_face_ids(face_id)
|
||||
)
|
||||
else:
|
||||
face_ids = [face_id]
|
||||
face_ids = sorted(set(face_ids or [face_id]))
|
||||
@@ -4431,6 +4642,423 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._same_domain_face_ids_cache[item] = list(face_ids)
|
||||
return list(face_ids)
|
||||
|
||||
def _asitus_hole_region_ids(self, face_id: int) -> list[int]:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
return []
|
||||
if not self._can_use_asitus_hole_recognition():
|
||||
return []
|
||||
if self._asitus_hole_regions_loading:
|
||||
return []
|
||||
if not self._asitus_hole_regions_attempted:
|
||||
self._load_asitus_hole_regions()
|
||||
return list(self._asitus_hole_region_cache.get(face_id, ()))
|
||||
|
||||
def _internal_hole_region_ids(self, face_id: int) -> list[int]:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
return []
|
||||
if not self._can_use_internal_recognition_graph():
|
||||
return []
|
||||
if not self._internal_hole_regions_attempted:
|
||||
self._load_internal_hole_regions()
|
||||
return list(self._internal_hole_region_cache.get(face_id, ()))
|
||||
|
||||
def _can_use_internal_recognition_graph(self) -> bool:
|
||||
return bool(self.faces)
|
||||
|
||||
def _load_internal_hole_regions(self) -> None:
|
||||
self._internal_hole_regions_attempted = True
|
||||
self._internal_hole_region_cache.clear()
|
||||
self._internal_recognition_info.clear()
|
||||
try:
|
||||
regions = recognize_through_hole_regions(self)
|
||||
mapped_groups: set[tuple[int, ...]] = set()
|
||||
for region in regions:
|
||||
group = tuple(sorted({int(item) for item in region.face_ids if 0 <= int(item) < len(self.faces)}))
|
||||
if len(group) < 2:
|
||||
continue
|
||||
mapped_groups.add(group)
|
||||
for face_id in group:
|
||||
self._internal_hole_region_cache[face_id] = list(group)
|
||||
self._internal_recognition_info.update(
|
||||
{
|
||||
"ok": True,
|
||||
"source": "internal-recognition-graph",
|
||||
"mapped_hole_count": len(mapped_groups),
|
||||
"mapped_hole_groups": tuple(sorted(mapped_groups)),
|
||||
"through_hole_region_count": len(regions),
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
self._internal_hole_region_cache.clear()
|
||||
self._internal_recognition_info.update(
|
||||
{
|
||||
"ok": False,
|
||||
"source": "internal-recognition-graph",
|
||||
"reason": "recognition-failed",
|
||||
"message": str(exc),
|
||||
}
|
||||
)
|
||||
|
||||
def _can_use_asitus_hole_recognition(self) -> bool:
|
||||
if int(getattr(self, "_topology_refresh_generation", 0) or 0) > 1:
|
||||
return False
|
||||
suffix = self.filename.suffix.lower()
|
||||
return suffix in {".step", ".stp"} and self.filename.is_file()
|
||||
|
||||
def _load_asitus_hole_regions(self) -> None:
|
||||
self.install_asitus_hole_recognition_result(run_asitus_hole_recognition(self.filename))
|
||||
|
||||
def begin_asitus_hole_region_load(self) -> bool:
|
||||
if not self._can_use_asitus_hole_recognition():
|
||||
return False
|
||||
if self._asitus_hole_regions_attempted or self._asitus_hole_regions_loading:
|
||||
return False
|
||||
self._asitus_hole_regions_loading = True
|
||||
self._asitus_hole_recognition_info.update(
|
||||
{
|
||||
"ok": False,
|
||||
"reason": "pending",
|
||||
"message": "Analysis Situs hole recognition is running in the background.",
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
def install_asitus_hole_recognition_result(self, result: object) -> list[tuple[int, ...]]:
|
||||
self._asitus_hole_regions_loading = False
|
||||
self._asitus_hole_regions_attempted = True
|
||||
self._asitus_hole_region_cache.clear()
|
||||
self._asitus_hole_recognition_info.clear()
|
||||
self._asitus_face_relation_cache.clear()
|
||||
self._asitus_adjacency_relation_cache.clear()
|
||||
self._asitus_geometric_relation_cache.clear()
|
||||
if not self._can_use_asitus_hole_recognition():
|
||||
self._asitus_hole_recognition_info.update(
|
||||
{
|
||||
"ok": False,
|
||||
"reason": "stale-topology",
|
||||
"message": "Analysis Situs result ignored because the model topology has changed.",
|
||||
}
|
||||
)
|
||||
return []
|
||||
if not isinstance(result, dict):
|
||||
result = {"ok": False, "reason": "bad-result", "message": "Analysis Situs returned an unexpected result."}
|
||||
self._asitus_hole_recognition_info.update(
|
||||
{
|
||||
"ok": bool(result.get("ok")),
|
||||
"reason": result.get("reason", ""),
|
||||
"message": result.get("message", ""),
|
||||
"cli": result.get("cli", ""),
|
||||
}
|
||||
)
|
||||
if not result.get("ok"):
|
||||
self._clear_same_domain_dependent_caches()
|
||||
return []
|
||||
self._install_asitus_relation_summary(result)
|
||||
raw_groups = result.get("groups", ())
|
||||
if not isinstance(raw_groups, (tuple, list)):
|
||||
self._clear_same_domain_dependent_caches()
|
||||
return []
|
||||
mapped_groups = self._map_asitus_hole_groups(raw_groups)
|
||||
if not mapped_groups:
|
||||
self._clear_same_domain_dependent_caches()
|
||||
return []
|
||||
for group in mapped_groups:
|
||||
for item in group:
|
||||
self._asitus_hole_region_cache[int(item)] = list(group)
|
||||
self._asitus_hole_recognition_info.update(
|
||||
{
|
||||
"mapped_hole_count": len(mapped_groups),
|
||||
"mapped_hole_groups": tuple(tuple(group) for group in mapped_groups),
|
||||
"source_face_id_base": "analysis-situs-aag-1-based",
|
||||
}
|
||||
)
|
||||
self._clear_same_domain_dependent_caches()
|
||||
return mapped_groups
|
||||
|
||||
def fail_asitus_hole_region_load(self, message: str) -> None:
|
||||
self._asitus_hole_regions_loading = False
|
||||
self._asitus_hole_regions_attempted = True
|
||||
self._asitus_hole_region_cache.clear()
|
||||
self._asitus_hole_recognition_info.clear()
|
||||
self._asitus_face_relation_cache.clear()
|
||||
self._asitus_adjacency_relation_cache.clear()
|
||||
self._asitus_geometric_relation_cache.clear()
|
||||
self._asitus_hole_recognition_info.update(
|
||||
{
|
||||
"ok": False,
|
||||
"reason": "worker-failed",
|
||||
"message": str(message or "Analysis Situs hole recognition failed."),
|
||||
}
|
||||
)
|
||||
self._clear_same_domain_dependent_caches()
|
||||
|
||||
def _clear_same_domain_dependent_caches(self) -> None:
|
||||
self._same_domain_face_ids_cache.clear()
|
||||
self._internal_hole_regions_attempted = False
|
||||
self._internal_hole_region_cache.clear()
|
||||
self._internal_recognition_info.clear()
|
||||
self._recognition_graph_cache.clear()
|
||||
self._through_hole_regions_cache.clear()
|
||||
self._quick_face_info_cache.clear()
|
||||
self._face_info_cache.clear()
|
||||
self._feature_info_cache.clear()
|
||||
self._face_first_level_topology_cache.clear()
|
||||
self._cylindrical_first_level_topology_cache.clear()
|
||||
self._face_first_level_fact_cache.clear()
|
||||
self._editable_feature_candidates_cache.clear()
|
||||
self._cylindrical_feature_candidates_cache.clear()
|
||||
|
||||
def _install_asitus_relation_summary(self, result: dict[str, object]) -> None:
|
||||
faces = result.get("faces", ())
|
||||
if isinstance(faces, (tuple, list)):
|
||||
for item in faces:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
raw_face_id = self._coerce_int(item.get("id"))
|
||||
face_id = self._map_asitus_single_face_id(raw_face_id)
|
||||
if face_id is None:
|
||||
continue
|
||||
neighbor_ids = tuple(
|
||||
sorted(
|
||||
{
|
||||
mapped
|
||||
for raw_neighbor in self._iter_int_values(item.get("neighbor_ids"))
|
||||
for mapped in (self._map_asitus_single_face_id(raw_neighbor),)
|
||||
if mapped is not None and mapped != face_id
|
||||
}
|
||||
)
|
||||
)
|
||||
self._asitus_face_relation_cache[face_id] = {
|
||||
"source": "analysis-situs-aag",
|
||||
"asitus_face_id": raw_face_id,
|
||||
"surface": str(item.get("surface") or ""),
|
||||
"neighbor_face_ids": neighbor_ids,
|
||||
"neighbor_face_count": len(neighbor_ids),
|
||||
}
|
||||
|
||||
adjacency = result.get("adjacency", ())
|
||||
if isinstance(adjacency, (tuple, list)):
|
||||
for item in adjacency:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
mapped_pair = tuple(
|
||||
sorted(
|
||||
{
|
||||
mapped
|
||||
for raw_face_id in self._iter_int_values(item.get("face_ids"))
|
||||
for mapped in (self._map_asitus_single_face_id(raw_face_id),)
|
||||
if mapped is not None
|
||||
}
|
||||
)
|
||||
)
|
||||
if len(mapped_pair) != 2:
|
||||
continue
|
||||
self._asitus_adjacency_relation_cache[mapped_pair] = {
|
||||
"source": "analysis-situs-aag",
|
||||
"face_ids": mapped_pair,
|
||||
"angle_type": str(item.get("angle_type") or ""),
|
||||
"angle_rad": item.get("angle_rad"),
|
||||
"edge_ids": tuple(self._iter_int_values(item.get("edge_ids"))),
|
||||
}
|
||||
|
||||
geometric_relations = result.get("geometric_relations", ())
|
||||
if isinstance(geometric_relations, (tuple, list)):
|
||||
for item in geometric_relations:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
mapped_pair = tuple(
|
||||
sorted(
|
||||
{
|
||||
mapped
|
||||
for raw_face_id in self._iter_int_values(item.get("face_ids"))
|
||||
for mapped in (self._map_asitus_single_face_id(raw_face_id),)
|
||||
if mapped is not None
|
||||
}
|
||||
)
|
||||
)
|
||||
if len(mapped_pair) != 2:
|
||||
continue
|
||||
relation = {
|
||||
"source": str(item.get("source") or "analysis-situs-probe"),
|
||||
"face_ids": mapped_pair,
|
||||
"relation_type": str(item.get("relation_type") or ""),
|
||||
"residual": item.get("residual"),
|
||||
}
|
||||
self._asitus_geometric_relation_cache.setdefault(mapped_pair, []).append(relation)
|
||||
|
||||
if (
|
||||
self._asitus_face_relation_cache
|
||||
or self._asitus_adjacency_relation_cache
|
||||
or self._asitus_geometric_relation_cache
|
||||
):
|
||||
self._asitus_hole_recognition_info.update(
|
||||
{
|
||||
"aag_face_relation_count": len(self._asitus_face_relation_cache),
|
||||
"aag_adjacency_relation_count": len(self._asitus_adjacency_relation_cache),
|
||||
"aag_geometric_relation_pair_count": len(self._asitus_geometric_relation_cache),
|
||||
"aag_geometric_relation_count": sum(
|
||||
len(items) for items in self._asitus_geometric_relation_cache.values()
|
||||
),
|
||||
"aag_surface_summary": dict(result.get("surface_summary", {}) or {}),
|
||||
"aag_angle_summary": dict(result.get("angle_summary", {}) or {}),
|
||||
"aag_geometric_relation_summary": dict(result.get("geometric_relation_summary", {}) or {}),
|
||||
"aag_geometric_relation_mode": str(result.get("geometric_relation_mode") or ""),
|
||||
"aag_relation_source": "analysis-situs-aag",
|
||||
}
|
||||
)
|
||||
|
||||
def _asitus_face_summary_fields(self, face_id: int) -> dict[str, object]:
|
||||
entry = self._asitus_face_relation_cache.get(int(face_id))
|
||||
if not entry:
|
||||
return {}
|
||||
neighbor_ids = tuple(int(item) for item in entry.get("neighbor_face_ids", ()) or ())
|
||||
relation_types: Counter[str] = Counter()
|
||||
for neighbor_id in neighbor_ids:
|
||||
pair = tuple(sorted((int(face_id), int(neighbor_id))))
|
||||
relation = self._asitus_adjacency_relation_cache.get(pair)
|
||||
if not relation:
|
||||
continue
|
||||
relation_type = str(relation.get("angle_type") or "adjacent")
|
||||
relation_types[relation_type] += 1
|
||||
relation_summary = ", ".join(f"{key}:{value}" for key, value in sorted(relation_types.items()))
|
||||
if not relation_summary:
|
||||
relation_summary = f"adjacent:{len(neighbor_ids)}"
|
||||
geometric_relation_types: Counter[str] = Counter()
|
||||
for pair, relations in self._asitus_geometric_relation_cache.items():
|
||||
if int(face_id) not in pair:
|
||||
continue
|
||||
for relation in relations:
|
||||
relation_type = str(relation.get("relation_type") or "")
|
||||
if relation_type:
|
||||
geometric_relation_types[relation_type] += 1
|
||||
geometric_relation_summary = ", ".join(
|
||||
f"{key}:{value}" for key, value in sorted(geometric_relation_types.items())
|
||||
)
|
||||
return {
|
||||
"asitus_relation_status": "ready",
|
||||
"asitus_relation_source": entry.get("source", "analysis-situs-aag"),
|
||||
"asitus_face_id": entry.get("asitus_face_id"),
|
||||
"asitus_surface": entry.get("surface", ""),
|
||||
"asitus_adjacent_face_ids": neighbor_ids,
|
||||
"asitus_adjacent_face_count": len(neighbor_ids),
|
||||
"asitus_adjacent_relation_summary": relation_summary,
|
||||
"asitus_geometric_relation_count": sum(geometric_relation_types.values()),
|
||||
"asitus_geometric_relation_summary": geometric_relation_summary,
|
||||
"asitus_geometric_relation_types": tuple(sorted(geometric_relation_types)),
|
||||
}
|
||||
|
||||
def _map_asitus_single_face_id(self, raw_face_id: object) -> int | None:
|
||||
value = self._coerce_int(raw_face_id)
|
||||
if value is None:
|
||||
return None
|
||||
if 1 <= value <= len(self.faces):
|
||||
return value - 1
|
||||
if 0 <= value < len(self.faces):
|
||||
return value
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _coerce_int(value: object) -> int | None:
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _iter_int_values(values: object) -> tuple[int, ...]:
|
||||
if values is None or isinstance(values, (str, bytes)):
|
||||
return ()
|
||||
try:
|
||||
iterator = iter(values) # type: ignore[arg-type]
|
||||
except TypeError:
|
||||
coerced = StepModel._coerce_int(values)
|
||||
return () if coerced is None else (coerced,)
|
||||
result: list[int] = []
|
||||
for item in iterator:
|
||||
coerced = StepModel._coerce_int(item)
|
||||
if coerced is not None:
|
||||
result.append(coerced)
|
||||
return tuple(result)
|
||||
|
||||
def _map_asitus_hole_groups(self, groups: Iterable[object]) -> list[tuple[int, ...]]:
|
||||
mapped: list[tuple[int, ...]] = []
|
||||
seen: set[tuple[int, ...]] = set()
|
||||
for raw_group in groups:
|
||||
try:
|
||||
values = tuple(sorted({int(item) for item in raw_group})) # type: ignore[arg-type]
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
group = self._map_asitus_hole_group(values)
|
||||
if not group or group in seen:
|
||||
continue
|
||||
seen.add(group)
|
||||
mapped.append(group)
|
||||
return mapped
|
||||
|
||||
def _map_asitus_hole_group(self, raw_group: tuple[int, ...]) -> tuple[int, ...]:
|
||||
best_group: tuple[int, ...] = ()
|
||||
best_score = -1.0
|
||||
for offset in (-1, 0):
|
||||
candidate = tuple(sorted({item + offset for item in raw_group}))
|
||||
score, cylinder_group = self._score_asitus_hole_group(candidate)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_group = cylinder_group
|
||||
return best_group if best_score >= 35.0 else ()
|
||||
|
||||
def _score_asitus_hole_group(self, candidate: tuple[int, ...]) -> tuple[float, tuple[int, ...]]:
|
||||
if not candidate:
|
||||
return -1.0, ()
|
||||
if any(face_id < 0 or face_id >= len(self.faces) for face_id in candidate):
|
||||
return -1.0, ()
|
||||
|
||||
cylinder_face_ids: list[int] = []
|
||||
cylinder_surfaces: list[BRepAdaptor_Surface] = []
|
||||
for face_id in candidate:
|
||||
try:
|
||||
surf = BRepAdaptor_Surface(self.faces[face_id])
|
||||
except Exception:
|
||||
continue
|
||||
if surf.GetType() != GeomAbs_Cylinder:
|
||||
continue
|
||||
cylinder_face_ids.append(face_id)
|
||||
cylinder_surfaces.append(surf)
|
||||
if not cylinder_face_ids:
|
||||
return -1.0, ()
|
||||
|
||||
score = 20.0 * len(cylinder_face_ids)
|
||||
if len(cylinder_face_ids) < len(candidate):
|
||||
score -= 5.0 * (len(candidate) - len(cylinder_face_ids))
|
||||
|
||||
part_ids = {self.face_part_ids[item] for item in cylinder_face_ids}
|
||||
solid_ids = {self.face_solid_ids[item] for item in cylinder_face_ids}
|
||||
if len(part_ids) == 1:
|
||||
score += 10.0
|
||||
if len(solid_ids) == 1:
|
||||
score += 10.0
|
||||
|
||||
if len(cylinder_surfaces) > 1:
|
||||
diagonal = _shape_diagonal(self.shape)
|
||||
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
|
||||
source = cylinder_surfaces[0]
|
||||
if all(_surfaces_are_cocylindrical(source, other, tolerance) for other in cylinder_surfaces[1:]):
|
||||
score += 40.0
|
||||
else:
|
||||
score -= 45.0
|
||||
|
||||
angular_span = 0.0
|
||||
for surf in cylinder_surfaces:
|
||||
try:
|
||||
angular_span += abs(float(surf.LastUParameter()) - float(surf.FirstUParameter()))
|
||||
except Exception:
|
||||
pass
|
||||
if angular_span >= math.tau * 0.85:
|
||||
score += 25.0
|
||||
elif angular_span >= math.pi * 0.9:
|
||||
score += 8.0
|
||||
|
||||
return score, tuple(sorted(set(cylinder_face_ids)))
|
||||
|
||||
def _connected_cocylindrical_face_ids(self, face_id: int) -> list[int]:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user