feat: 完善 Face 一级编辑与隔离计算

This commit is contained in:
2026-08-05 15:08:16 +08:00
parent a76282d7dd
commit ed1faf51d8
28 changed files with 3740 additions and 876 deletions
+70 -34
View File
@@ -74,7 +74,7 @@ from OCC.Core.XCAFDoc import XCAFDoc_DocumentTool
from OCC.Core.gp import gp_Ax1, gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf, gp_Vec
from OCC.Extend.TopologyUtils import TopologyExplorer, discretize_edge
from .constants import CURVE_TYPES, SNAPSHOT_FACE_LOGICAL_IDS_KEY, SURFACE_TYPES
from .constants import CURVE_TYPES, FREEFORM_FACE_SURFACES, SNAPSHOT_FACE_LOGICAL_IDS_KEY, SURFACE_TYPES
from .export import ExportMixin
from .features import FeatureMixin
from .operations import OperationMixin
@@ -352,19 +352,20 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
info["normal"] = _dir_tuple(direction)
info["oriented_normal"] = _oriented_dir_tuple(direction, face)
info["push_pull_confidence"] = "unchecked"
info["push_pull_note"] = "快速选择阶段不判断材料内外方向;执行拉时会重新计算。"
cap_direction = self._cylindrical_cap_push_pull_direction(face_id, surf)
if cap_direction is not None:
info.update(cap_direction)
info["push_pull_note"] = "快速选择阶段不判断材料内外方向;执行拉伸/切除时会重新计算。"
# Keep ordinary selection cheap. Cylindrical-cap direction can
# scan adjacent surfaces and cost hundreds of ms on large STEP
# files; the full push/pull plan recomputes it when the user
# actually edits.
info["push_pull_status"] = "candidate"
info["feature_type"] = "拉平面候选"
info["feature_type"] = "可拉伸/切除平面候选"
info["feature_source_face_id"] = face_id
info["feature_highlight_face_ids"] = (face_id,)
info["feature_edit_actions"] = "拉平面"
info["feature_edit_actions"] = "伸/切除平面"
if bool(info.get("has_inner_boundaries")):
info["local_face_deform_ready"] = False
info["local_face_deform_face_count"] = 1
info["local_face_deform_blocker"] = "当前 Face 有内孔/内边界;请优先使用推拉当前面、孔或槽的专门修改入口。"
info["local_face_deform_blocker"] = "当前 Face 有内孔/内边界;请优先使用拉伸/切除、孔或槽的专门修改入口。"
else:
info["local_face_deform_ready"] = True
info["local_face_deform_face_count"] = 1
@@ -430,6 +431,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
info["minor_radius"] = torus.MinorRadius()
info["feature_torus_major_radius"] = torus.MajorRadius()
info["feature_torus_minor_radius"] = torus.MinorRadius()
elif surface_label in FREEFORM_FACE_SURFACES:
info.update(self._freeform_face_limit_fields(surface_label))
info.update(self._recognition_summary_fields(info))
self._quick_face_info_cache[face_id] = dict(info)
@@ -528,10 +531,33 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
info["axis"] = _dir_tuple(torus.Axis().Direction())
info["major_radius"] = torus.MajorRadius()
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._recognition_summary_fields(info))
self._face_info_cache[face_id] = dict(info)
return dict(info)
def _freeform_face_limit_fields(self, surface: str) -> dict[str, object]:
blocker = (
"当前 Face 是自由曲面/非基础解析曲面;STEP 里通常没有可直接修改的历史参数,"
"当前一级阶段不开放面积、尺寸、半径或偏移类参数化修改。"
"请先保留为只读诊断,后续需要专门的曲面控制点或曲面替换语义。"
)
return {
"feature_type": "自由曲面 Face(暂不支持参数化编辑)",
"feature_edit_actions": "只读诊断;不开放参数化修改",
"freeform_face_status": "blocked",
"freeform_face_risk": "blocked",
"freeform_face_blockers": blocker,
"local_face_deform_ready": False,
"local_face_deform_face_count": 1,
"local_face_deform_blocker": blocker,
"recognition_confidence": "low",
"recognition_risk": "blocked",
"recognition_blockers": blocker,
"note": f"{surface} 当前按自由曲面处理,不会伪装成平面、圆柱、圆锥、球面或环面参数。",
}
def face_surface_kind(self, face_id: int) -> str:
if face_id < 0 or face_id >= len(self.faces):
return ""
@@ -588,12 +614,14 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
confidence = "high"
elif surface == "torus" and _float_or_none(info.get("major_radius")) is not None and _float_or_none(info.get("minor_radius")) is not None:
confidence = "high"
if surface in FREEFORM_FACE_SURFACES:
confidence = "low"
risk_rank = {"low": 0, "medium": 1, "high": 2, "blocked": 3}
capability_specs = (
("resize_status", "resize_risk", "resize_blockers", "孔/槽/圆柱直径"),
("push_pull_status", "push_pull_risk", "push_pull_blockers", "平面"),
("shell_status", "shell_risk", "shell_blockers", "薄壁厚度"),
("push_pull_status", "push_pull_risk", "push_pull_blockers", "平面拉伸/切除"),
("shell_status", "shell_risk", "shell_blockers", "壳体厚度"),
("cylinder_resize_status", "cylinder_resize_risk", "cylinder_resize_blockers", "圆柱直径/半径"),
("boss_resize_status", "boss_resize_risk", "boss_resize_blockers", "圆柱凸台直径/高度/轴心"),
("depth_status", "depth_risk", "depth_blockers", "盲孔/盲槽深度"),
@@ -611,8 +639,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
has_planar_push_pull_candidate = (
surface == "plane"
and (
"" in feature_type_text
or "" in feature_actions_text
"伸/切除" in feature_type_text
or "伸/切除" in feature_actions_text
or str(info.get("push_pull_status") or "").strip() == "candidate"
)
)
@@ -632,10 +660,13 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
for key in (
"risk",
"first_level_topology_risk",
"freeform_face_risk",
):
value = str(info.get(key) or "").strip()
if risk_rank.get(value, -1) > risk_rank.get(risk, -1):
risk = value
if surface in FREEFORM_FACE_SURFACES:
risk = "blocked"
for status_key, risk_key, _blocker_key, _label in capability_specs:
value = str(info.get(risk_key) or "").strip()
if value == "blocked" and has_available_capability:
@@ -696,11 +727,11 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
bottom_count = len(tuple(info.get("feature_bottom_face_ids") or ()))
except TypeError:
bottom_count = 0
add("bottom_faces", f"疑似底面Face={bottom_count}")
add("bottom_faces", f"疑似底面 Face={bottom_count}")
if info.get("slot_status") == "candidate":
add("slot_geometry", "部分圆柱槽/半孔几何")
if info.get("shell_region_status") == "candidate":
add("shell_opposite_face", "找到相对平面/薄壁候选")
add("shell_opposite_face", "找到相对平面/壳体候选")
ready_actions: list[str] = []
limited_actions: list[str] = []
@@ -724,13 +755,13 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
add_action(limited_actions, label)
if has_planar_push_pull_candidate:
add_action(ready_actions, "平面")
add_action(ready_actions, "平面拉伸/切除")
if has_local_face_deform:
add_action(ready_actions, "当前面面积/面宽/面高/中心/偏移")
add_action(ready_actions, "当前面面积/U向尺寸/V向尺寸/中心/偏移")
elif str(info.get("local_face_deform_blocker") or "").strip():
add_action(limited_actions, "当前面局部尺寸/中心/偏移")
add_action(limited_actions, "局部重建尺寸/中心/偏移")
if has_shell_candidate:
add_action(ready_actions, "薄壁厚度")
add_action(ready_actions, "壳体厚度")
if has_slot_candidate:
add_action(ready_actions, "槽/半孔宽度/深度/弧长")
if surface == "cone" and feature_type_text:
@@ -1079,25 +1110,25 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
shell_info = self._planar_shell_region_info(face_id, coplanar_face_ids, info)
prismatic_info = self._planar_rectangular_profile_info(face_id, coplanar_face_ids, info, shell_info)
if len(coplanar_face_ids) > 1:
scope_note = f"已检测到 {len(coplanar_face_ids)} 个共面且相接/重叠的 face拉时会作为同一片平面区域处理。"
scope_note = f"已检测到 {len(coplanar_face_ids)} 个共面且相接/重叠的 face,拉伸/切除时会作为同一片平面区域处理。"
else:
scope_note = "当前 face 没有检测到可一起拉的共面相接/重叠邻居。"
edit_actions = "拉平面"
scope_note = "当前 face 没有检测到可一起拉伸/切除的共面相接/重叠邻居。"
edit_actions = "伸/切除平面"
if shell_info.get("shell_region_status") == "candidate":
edit_actions += ";调整薄壁/壳体厚度"
edit_actions += ";调整壳体厚度"
if prismatic_info.get("prismatic_profile_status") == "candidate":
edit_actions = "调整规则矩形轮廓长度/宽度"
if prismatic_info.get("prismatic_extrusion_status") == "candidate":
edit_actions += ";调整棱柱高度/凹槽深度"
else:
edit_actions += ";沿法向"
edit_actions += ";沿法向拉伸/切除"
highlight_face_ids = set(coplanar_face_ids)
highlight_face_ids.update(_int_values(prismatic_info.get("prismatic_highlight_face_ids")))
result = dict(info)
result.update(
{
"kind": "feature",
"feature_type": "拉平面候选",
"feature_type": "可拉伸/切除平面候选",
"feature_source_face_id": face_id,
"feature_face_ids": tuple(coplanar_face_ids),
"feature_highlight_face_ids": tuple(coplanar_face_ids),
@@ -1363,7 +1394,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
except Exception:
return {
"shell_region_status": "not-detected",
"shell_region_note": "无法读取当前平面,不能估算薄壁/壳体区域。",
"shell_region_note": "无法读取当前平面,不能估算壳体区域。",
}
if source_surf.GetType() != GeomAbs_Plane:
return {}
@@ -1382,7 +1413,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
if source_interval is None:
return {
"shell_region_status": "not-detected",
"shell_region_note": "当前平面区域缺少稳定投影范围,不能估算薄壁/壳体厚度。",
"shell_region_note": "当前平面区域缺少稳定投影范围,不能估算壳体厚度。",
}
def interval_length(interval: tuple[float, float]) -> float:
@@ -1440,7 +1471,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
if best is None:
return {
"shell_region_status": "not-detected",
"shell_region_note": "未找到与当前平面投影重叠的相对平面;暂不能估算局部壳体/薄壁厚度。",
"shell_region_note": "未找到与当前平面投影重叠的相对平面;暂不能估算局部壳体厚度。",
}
thickness = float(best["shell_thickness_estimate"])
@@ -1454,7 +1485,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
"shell_opposite_face_id": best["shell_opposite_face_id"],
"shell_thickness_estimate": thickness,
"shell_overlap_ratio_estimate": overlap_ratio,
"shell_region_note": "相对平面距离明显大于当前面的局部短边,不按薄壁厚度处理。",
"shell_region_note": "相对平面距离明显大于当前面的局部短边,不按壳体厚度处理。",
}
thin_ratio = thickness / diagonal
if overlap_ratio >= 0.55 and thin_ratio <= 0.08:
@@ -1471,7 +1502,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
"shell_source_face_ids": tuple(valid_region_ids),
**best,
"shell_note": (
"通过同一 solid 内投影重叠的相对平面估算薄壁/壳体厚度;"
"通过同一 solid 内投影重叠的相对平面估算壳体厚度;"
"这是 B-Rep 几何近似,不等同于原 CAD 壳命令参数。"
),
}
@@ -1501,18 +1532,23 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
opening_face_ids = end_faces["opening_face_ids"]
guess = str(info.get("feature_guess", "cylindrical face"))
has_two_axial_caps = bool(end_faces["start_end_face_ids"] and end_faces["end_end_face_ids"])
support_face_ids_for_round = sorted(set(adjacent_face_ids) - set(end_face_ids))
material_toward = str(info.get("material_toward_axis", "") or "")
material_away = str(info.get("material_away_axis", "") or "")
complete_cylinder_with_caps = combined_angular_span >= math.tau * 0.92 and has_two_axial_caps
if (
guess == "round/fillet candidate"
and has_two_axial_caps
and material_toward == "inside"
and "outside" in material_away
and (complete_cylinder_with_caps or len(support_face_ids_for_round) < 2)
):
info = dict(info)
info["feature_guess"] = "boss/outer-round candidate"
info["confidence"] = "medium"
info["note"] = "partial cylinder has material inside its axis and explicit planar caps at both ends"
info["note"] = (
"cocylindrical region has material inside its axis and explicit planar caps at both ends"
)
domain_info["feature_guess"] = info["feature_guess"]
slot_info = self._cylindrical_slot_info(face_id, adjacent_face_ids, end_face_ids, domain_info)
fillet_info = self._cylindrical_existing_fillet_info(face_id, adjacent_face_ids, end_face_ids, domain_info)
@@ -2261,7 +2297,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
{
"local_face_deform_ready": False,
"local_face_deform_face_count": 1,
"local_face_deform_blocker": "找不到当前 Face 所属 Solid,不能做“只改当前面”的局部重建。",
"local_face_deform_blocker": "找不到当前 Face 所属 Solid,不能做局部重建。",
}
)
solid_face_ids = [index for index, item in enumerate(self.face_solid_ids) if item == solid_id]
@@ -2273,7 +2309,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
{
"local_face_deform_ready": False,
"local_face_deform_face_count": face_count,
"local_face_deform_blocker": "所属 Solid 的 Face 数量超过 128,当前版本不开放“只改当前面”的局部重建。",
"local_face_deform_blocker": "所属 Solid 的 Face 数量超过 128,当前版本不开放局部重建。",
}
)
@@ -2296,7 +2332,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
{
"local_face_deform_ready": False,
"local_face_deform_face_count": face_count,
"local_face_deform_blocker": "所属 Solid 含有曲面,当前版本只对简单全平面 Solid 开放“只改当前面”",
"local_face_deform_blocker": "所属 Solid 含有曲面,当前版本只对简单全平面 Solid 开放局部重建",
}
)
wire_info = self._face_boundary_wire_info(face)
@@ -2305,7 +2341,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
{
"local_face_deform_ready": False,
"local_face_deform_face_count": face_count,
"local_face_deform_blocker": "所属 Solid 里有带内孔/内边界的 Face,请优先使用孔、槽或拉等专门修改方式。",
"local_face_deform_blocker": "所属 Solid 里有带内孔/内边界的 Face,请优先使用孔、槽或拉伸/切除等专门修改方式。",
}
)
try: