feat: 增强一级共面同轴事实识别
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user