feat: 增强一级共面同轴事实识别

This commit is contained in:
2026-08-10 18:05:18 +08:00
parent 3a851d0967
commit 3aadd6a34e
5 changed files with 329 additions and 2 deletions
+190
View File
@@ -3299,6 +3299,152 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
),
}
def _first_level_same_domain_relation_fields(
self,
*,
face_id: int,
same_domain_face_ids: Iterable[int],
relation_scope: str = "face",
) -> dict[str, object]:
domain_ids = tuple(sorted({int(item) for item in same_domain_face_ids if 0 <= int(item) < len(self.faces)}))
surface = self.face_surface_kind(face_id) if 0 <= int(face_id) < len(self.faces) else "unknown"
fragment_ids = tuple(item for item in domain_ids if item != int(face_id))
if surface == "plane":
relation = "coplanar"
relation_label = "共面"
elif surface == "cylinder":
relation = "cocylindrical"
relation_label = "同圆柱面"
elif len(domain_ids) > 1:
relation = "same-domain"
relation_label = "同域"
else:
relation = "single-face"
relation_label = "单 Face"
if len(domain_ids) > 1:
summary = f"一级同域关系:已识别 {len(domain_ids)}{relation_label} Face,会作为同一局部主体。"
elif surface in {"plane", "cylinder"}:
summary = f"一级同域关系:当前为单个{relation_label}主体,未发现可同步碎片 Face。"
else:
summary = "一级同域关系:当前曲面类型暂不支持同域碎片同步,只按单 Face 处理。"
return {
"first_level_same_domain_status": "ready",
"first_level_same_domain_scope": relation_scope,
"first_level_same_domain_relation": relation,
"first_level_same_domain_relation_label": relation_label,
"first_level_same_domain_surface": surface,
"first_level_same_domain_face_ids": domain_ids,
"first_level_same_domain_face_count": len(domain_ids),
"first_level_same_domain_fragment_face_ids": fragment_ids,
"first_level_same_domain_fragment_face_count": len(fragment_ids),
"first_level_same_domain_summary": summary,
"first_level_same_domain_note": (
"同域/共面/同圆柱面碎片属于当前一级主体;当前只同步明确支持的编辑路径,"
"不会递归扩展到二级、三级邻域。"
),
}
def _cylinders_are_coaxial(
self,
left: BRepAdaptor_Surface,
right: BRepAdaptor_Surface,
tolerance: float,
) -> bool:
if left.GetType() != GeomAbs_Cylinder or right.GetType() != GeomAbs_Cylinder:
return False
left_axis = left.Cylinder().Axis()
right_axis = right.Cylinder().Axis()
axis_dot = abs(_direction_dot(left_axis.Direction(), right_axis.Direction()))
if axis_dot < 1.0 - 1e-6:
return False
distance = _point_axis_distance(left_axis.Location(), left_axis.Direction(), right_axis.Location())
return distance <= max(tolerance, _shape_diagonal(self.shape) * 1e-7, 1e-6)
def _first_level_coaxial_cylinder_relation_fields(
self,
*,
subject_face_ids: Iterable[int],
adjacent_face_ids: Iterable[int],
relation_scope: str = "face",
) -> dict[str, object]:
involved_ids = tuple(
sorted({int(item) for item in (*tuple(subject_face_ids), *tuple(adjacent_face_ids)) if 0 <= int(item) < len(self.faces)})
)
diagonal = _shape_diagonal(self.shape)
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
cylinder_infos: dict[int, dict[str, object]] = {}
for face_id in involved_ids:
surf = BRepAdaptor_Surface(self.faces[face_id])
if surf.GetType() != GeomAbs_Cylinder:
continue
cylinder = surf.Cylinder()
axis = cylinder.Axis()
cylinder_infos[face_id] = {
"face_id": face_id,
"axis_point": _point_tuple(axis.Location()),
"axis_direction": _dir_tuple(axis.Direction()),
"radius": float(cylinder.Radius()),
}
rows: list[dict[str, object]] = []
cylinder_ids = tuple(sorted(cylinder_infos))
for left_index, left_id in enumerate(cylinder_ids):
left_surf = BRepAdaptor_Surface(self.faces[left_id])
for right_id in cylinder_ids[left_index + 1 :]:
right_surf = BRepAdaptor_Surface(self.faces[right_id])
if not self._cylinders_are_coaxial(left_surf, right_surf, tolerance):
continue
left_radius = float(cylinder_infos[left_id]["radius"])
right_radius = float(cylinder_infos[right_id]["radius"])
rows.append(
{
"relation_kind": "cylinder-cylinder-axis",
"source": "first-level-cylinder-pair",
"face_ids": (left_id, right_id),
"relation": "coaxial",
"relation_label": "同轴",
"radii": (left_radius, right_radius),
"same_radius": abs(left_radius - right_radius) <= max(tolerance, max(left_radius, right_radius) * 1e-6),
}
)
related_face_ids = tuple(sorted({face_id for row in rows for face_id in _int_values(row.get("face_ids"))}))
if rows:
unique_radii = []
for face_id in related_face_ids:
radius = float(cylinder_infos[face_id]["radius"])
if not any(abs(radius - existing) <= max(tolerance, max(radius, existing) * 1e-6) for existing in unique_radii):
unique_radii.append(radius)
summary = (
f"一级同轴圆柱关系:已识别 {len(rows)} 组同轴圆柱 Face"
f"相关 Face {len(related_face_ids)},半径 {tuple(round(item, 6) for item in sorted(unique_radii))}"
)
status = "ready"
elif cylinder_infos:
summary = "一级同轴圆柱关系:一级范围内只有单个圆柱 Face,暂时没有可配对同轴关系。"
status = "unavailable"
else:
summary = "一级同轴圆柱关系:一级范围内没有圆柱 Face。"
status = "unavailable"
return {
"first_level_coaxial_cylinder_status": status,
"first_level_coaxial_cylinder_scope": relation_scope,
"first_level_coaxial_cylinder_rows": tuple(rows),
"first_level_coaxial_cylinder_count": len(rows),
"first_level_coaxial_cylinder_face_ids": related_face_ids,
"first_level_coaxial_cylinder_face_count": len(related_face_ids),
"first_level_coaxial_cylinder_candidate_face_ids": cylinder_ids,
"first_level_coaxial_cylinder_candidate_face_count": len(cylinder_ids),
"first_level_coaxial_cylinder_summary": summary,
"first_level_coaxial_cylinder_note": (
"这些是一级范围内圆柱轴线重合的事实;当前只作为识别和守门证据,"
"跨特征同轴传播仍不自动执行。"
),
}
def face_first_level_topology(self, face_id: int) -> dict[str, object]:
"""Return the explicit first-level B-Rep neighborhood for a Face.
@@ -3367,6 +3513,16 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
adjacent_face_ids=adjacent_face_ids,
relation_scope="face",
)
same_domain_relation_fields = self._first_level_same_domain_relation_fields(
face_id=face_id,
same_domain_face_ids=same_domain_face_ids,
relation_scope="face",
)
coaxial_cylinder_relation_fields = self._first_level_coaxial_cylinder_relation_fields(
subject_face_ids=same_domain_face_ids,
adjacent_face_ids=adjacent_face_ids,
relation_scope="face",
)
topology = {
"topology_relation_model": "STEP/B-Rep shared-edge first-level",
"topology_relation_depth": 1,
@@ -3395,6 +3551,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
"first_level_face_ids": first_level_face_ids,
"first_level_face_count": len(first_level_face_ids),
**planar_relation_fields,
**same_domain_relation_fields,
**coaxial_cylinder_relation_fields,
"first_level_topology_note": (
f"已识别当前 Face 区域 {len(same_domain_face_ids)} 个 Face、"
f"边界 Edge {len(region_boundary_edge_ids)} 条、"
@@ -3498,6 +3656,16 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
for adjacent_id, edge_ids in sorted(shared_edges_by_face.items())
)
same_domain_relation_fields = self._first_level_same_domain_relation_fields(
face_id=face_id,
same_domain_face_ids=side_face_ids,
relation_scope="cylindrical-feature",
)
coaxial_cylinder_relation_fields = self._first_level_coaxial_cylinder_relation_fields(
subject_face_ids=side_face_ids,
adjacent_face_ids=adjacent_face_ids,
relation_scope="cylindrical-feature",
)
angular_span = feature.get("slot_angular_span", feature.get("angular_span", info.get("angular_span")))
topology = {
"topology_relation_model": "STEP/B-Rep cylindrical-feature shared-edge first-level",
@@ -3540,6 +3708,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
"cylindrical_feature_opening_face_count": len(opening_face_ids),
"cylindrical_feature_slot_boundary_face_ids": slot_boundary_face_ids,
"cylindrical_feature_slot_boundary_face_count": len(slot_boundary_face_ids),
**same_domain_relation_fields,
**coaxial_cylinder_relation_fields,
"first_level_topology_note": (
f"Cylindrical side Faces={len(side_face_ids)}, boundary Edges={len(region_boundary_edge_ids)}, "
f"boundary Vertices={len(boundary_vertex_points)}, direct adjacent Faces={len(adjacent_face_ids)}. "
@@ -3647,6 +3817,16 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
for key, value in topology.items()
if str(key).startswith("first_level_planar_relation_")
}
same_domain_relation_fields = {
key: value
for key, value in topology.items()
if str(key).startswith("first_level_same_domain_")
}
coaxial_cylinder_relation_fields = {
key: value
for key, value in topology.items()
if str(key).startswith("first_level_coaxial_cylinder_")
}
if not planar_relation_fields:
planar_relation_fields = self._first_level_planar_relation_fields(
subject_face_ids=subject_face_ids,
@@ -3654,13 +3834,21 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
relation_scope=resolved_scope,
)
planar_summary = str(planar_relation_fields.get("first_level_planar_relation_summary") or "").strip()
same_domain_summary = str(same_domain_relation_fields.get("first_level_same_domain_summary") or "").strip()
coaxial_summary = str(
coaxial_cylinder_relation_fields.get("first_level_coaxial_cylinder_summary") or ""
).strip()
summary = (
f"{subject_role}: Face {len(subject_face_ids)}, boundary Edge {len(boundary_edge_ids)}, "
f"boundary Vertex {len(boundary_vertex_points)}, direct adjacent Face {len(adjacent_face_ids)}; "
"deeper relations are recorded as future propagation targets, not edited automatically."
)
if same_domain_summary and int(same_domain_relation_fields.get("first_level_same_domain_face_count", 0) or 0) > 1:
summary = f"{summary} {same_domain_summary}"
if planar_summary and planar_relation_fields.get("first_level_planar_relation_status") == "ready":
summary = f"{summary} {planar_summary}"
if coaxial_summary and coaxial_cylinder_relation_fields.get("first_level_coaxial_cylinder_status") == "ready":
summary = f"{summary} {coaxial_summary}"
facts = {
"first_level_fact_model": "STEP/B-Rep first-level fact graph",
"first_level_fact_source_model": source_model,
@@ -3686,6 +3874,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
"first_level_fact_ignored_relation_note": topology.get("topology_ignored_relation_note", ""),
"first_level_fact_summary": summary,
**planar_relation_fields,
**same_domain_relation_fields,
**coaxial_cylinder_relation_fields,
}
for item in facts["first_level_fact_subject_face_ids"]:
self._face_first_level_fact_cache[(int(item), resolved_scope)] = dict(facts)