feat: 完善 Face 一级关系编辑和稳定性校验

This commit is contained in:
2026-08-04 18:15:29 +08:00
parent 5799d5d813
commit a76282d7dd
28 changed files with 6872 additions and 270 deletions
+137
View File
@@ -64,7 +64,41 @@ from .geometry_utils import * # noqa: F403
class FeatureMixin:
def _first_level_fact_plan_fields(self, face_id: int, scope: str) -> dict[str, object]:
try:
return self.face_first_level_facts(face_id, scope=scope)
except Exception as exc:
resolved_scope = str(scope or "face")
return {
"first_level_fact_model": "STEP/B-Rep first-level fact graph",
"first_level_fact_source_model": resolved_scope,
"first_level_fact_status": "unavailable",
"first_level_fact_relation_depth": 1,
"first_level_fact_relation_boundary": "shared-edge",
"first_level_fact_scope": resolved_scope,
"first_level_fact_subject_role": "selected Face",
"first_level_fact_subject_face_ids": (face_id,),
"first_level_fact_subject_face_count": 1,
"first_level_fact_boundary_edge_ids": (),
"first_level_fact_boundary_edge_count": 0,
"first_level_fact_boundary_vertex_points": (),
"first_level_fact_boundary_vertex_count": 0,
"first_level_fact_adjacent_face_ids": (),
"first_level_fact_adjacent_face_count": 0,
"first_level_fact_adjacent_surface_types": (),
"first_level_fact_shared_edges_by_face": (),
"first_level_fact_included_face_ids": (face_id,),
"first_level_fact_included_face_count": 1,
"first_level_fact_role_groups": (),
"first_level_fact_ignored_relation_depths": ("second-level", "third-level", "deeper"),
"first_level_fact_ignored_relation_note": (
"Only first-level shared-edge relations are considered in this stage."
),
"first_level_fact_summary": f"当前 Face 的一级事实图暂时无法生成:{exc}",
}
def _cylindrical_feature_first_level_plan_fields(self, face_id: int) -> dict[str, object]:
fact_fields = self._first_level_fact_plan_fields(face_id, "cylindrical-feature")
try:
topology = self.cylindrical_feature_first_level_topology(face_id)
except Exception as exc:
@@ -80,6 +114,7 @@ class FeatureMixin:
"first_level_adjacent_face_count": 0,
"cylindrical_feature_side_face_ids": (face_id,),
"cylindrical_feature_side_face_count": 1,
**fact_fields,
}
fields = {
@@ -121,6 +156,7 @@ class FeatureMixin:
0,
),
}
fields.update(fact_fields)
fields["first_level_edit_semantics"] = (
"Cylindrical feature edits currently use only first-level shared-edge topology: "
"the selected cylinder/slot wall is rebuilt together with its direct boundary neighbors; "
@@ -2394,6 +2430,8 @@ class FeatureMixin:
"solid_id": info["solid_id"],
"diameter": info.get("diameter"),
"radius": info.get("radius"),
"axis": info.get("axis"),
"axis_point": info.get("axis_point"),
"angular_span": info.get("angular_span"),
"height_estimate": scoped_info.get("height_estimate"),
"feature_type": feature.get("feature_type"),
@@ -3067,11 +3105,110 @@ class FeatureMixin:
)
return samples
def _cylindrical_cap_push_pull_direction(
self,
face_id: int,
surf: BRepAdaptor_Surface,
) -> dict[str, object] | None:
if face_id < 0 or face_id >= len(self.faces):
return None
if surf.GetType() != GeomAbs_Plane:
return None
plane_point = surf.Plane().Location()
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
adjacent_face_ids = self._adjacent_face_ids_for_edges(boundary_edge_ids, face_id)
if not adjacent_face_ids:
return None
candidates: list[tuple[float, tuple[float, float, float], dict[str, object]]] = []
diagonal = _shape_diagonal(self.shape)
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
for adjacent_id in adjacent_face_ids:
if adjacent_id < 0 or adjacent_id >= len(self.faces):
continue
try:
side_surf = BRepAdaptor_Surface(self.faces[adjacent_id])
except Exception:
continue
if side_surf.GetType() != GeomAbs_Cylinder:
continue
cylinder = side_surf.Cylinder()
radius = max(float(cylinder.Radius()), 0.0)
axis = cylinder.Axis()
axis_point = axis.Location()
axis_dir = axis.Direction()
try:
axis_range = self._cylindrical_axis_range(adjacent_id, side_surf)
except Exception:
axis_range = {
"v_min": min(float(side_surf.FirstVParameter()), float(side_surf.LastVParameter())),
"v_max": max(float(side_surf.FirstVParameter()), float(side_surf.LastVParameter())),
}
v_min = float(axis_range["v_min"])
v_max = float(axis_range["v_max"])
height = max(v_max - v_min, 1e-9)
cap_parameter = _axis_parameter(axis_point, axis_dir, plane_point)
start_distance = abs(cap_parameter - v_min)
end_distance = abs(cap_parameter - v_max)
end_tolerance = max(height * 0.05, radius * 0.2, tolerance * 10.0, 0.05)
if start_distance <= end_distance and start_distance <= end_tolerance:
outward = _neg_tuple(_dir_tuple(axis_dir))
end_label = "start"
score = start_distance
elif end_distance <= end_tolerance:
outward = _dir_tuple(axis_dir)
end_label = "end"
score = end_distance
else:
continue
candidates.append(
(
score,
outward,
{
"cap_axis_face_id": adjacent_id,
"cap_axis_end": end_label,
"cap_axis_parameter": cap_parameter,
"cap_axis_start_parameter": v_min,
"cap_axis_end_parameter": v_max,
},
)
)
if not candidates:
return None
candidates.sort(key=lambda item: item[0])
_score, outward, details = candidates[0]
for _other_score, other_outward, _other_details in candidates[1:]:
if _tuple_dot(outward, other_outward) < 0.92:
return None
return {
"outward_direction": outward,
"inward_direction": _neg_tuple(outward),
"plus_side_state": "cylindrical-cap-axis",
"minus_side_state": "cylindrical-cap-axis",
"confidence": "high",
"note": "cylindrical cap direction inferred from adjacent cylinder axis",
"push_pull_outward_direction": outward,
"push_pull_inward_direction": _neg_tuple(outward),
"push_pull_plus_side": "cylindrical-cap-axis",
"push_pull_minus_side": "cylindrical-cap-axis",
"push_pull_confidence": "high",
"push_pull_note": "cylindrical cap direction inferred from adjacent cylinder axis",
**details,
}
def _plane_push_pull_direction(self, face_id: int, surf: BRepAdaptor_Surface) -> dict[str, object]:
face = self.faces[face_id]
direction = surf.Plane().Axis().Direction()
axis_tuple = _dir_tuple(direction)
oriented_tuple = _oriented_dir_tuple(direction, face)
cap_direction = self._cylindrical_cap_push_pull_direction(face_id, surf)
if cap_direction is not None:
return cap_direction
fallback = {
"outward_direction": oriented_tuple,
"inward_direction": _neg_tuple(oriented_tuple),