feat: 完善特征编辑和选择稳定性
This commit is contained in:
+175
-25
@@ -98,6 +98,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self.edge_solid_ids: list[int] = []
|
||||
self.solids: list[tuple[int, TopoDS_Shape]] = []
|
||||
self._face_info_cache: dict[int, dict[str, object]] = {}
|
||||
self._feature_info_cache: dict[int, dict[str, object]] = {}
|
||||
self._edge_info_cache: dict[int, dict[str, object]] = {}
|
||||
self._face_edge_ids_cache: dict[int, list[int]] = {}
|
||||
self._edge_face_ids_cache: dict[int, list[int]] = {}
|
||||
@@ -176,6 +177,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self.edge_solid_ids.clear()
|
||||
self.solids.clear()
|
||||
self._face_info_cache.clear()
|
||||
self._feature_info_cache.clear()
|
||||
self._edge_info_cache.clear()
|
||||
self._face_edge_ids_cache.clear()
|
||||
self._edge_face_ids_cache.clear()
|
||||
@@ -268,6 +270,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
if isinstance(logical_ids, (list, tuple)) and len(logical_ids) == len(self.faces):
|
||||
self.face_logical_ids = [int(item) for item in logical_ids]
|
||||
self._face_info_cache.clear()
|
||||
self._feature_info_cache.clear()
|
||||
self._same_domain_face_ids_cache.clear()
|
||||
|
||||
def face_info(self, face_id: int) -> dict[str, object]:
|
||||
@@ -356,27 +359,140 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_info_cache[face_id] = dict(info)
|
||||
return dict(info)
|
||||
|
||||
def face_surface_kind(self, face_id: int) -> str:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
return ""
|
||||
cached = self._face_info_cache.get(face_id)
|
||||
if cached is not None and cached.get("surface") is not None:
|
||||
return str(cached.get("surface"))
|
||||
try:
|
||||
surf = BRepAdaptor_Surface(self.faces[face_id])
|
||||
surface_type = surf.GetType()
|
||||
except Exception:
|
||||
return ""
|
||||
return SURFACE_TYPES.get(surface_type, f"type {surface_type}")
|
||||
|
||||
def face_cylinder_diameter(self, face_id: int) -> float | None:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
return None
|
||||
cached = self._face_info_cache.get(face_id)
|
||||
if cached is not None and cached.get("diameter") is not None and cached.get("surface") == "cylinder":
|
||||
try:
|
||||
return float(cached["diameter"])
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
try:
|
||||
surf = BRepAdaptor_Surface(self.faces[face_id])
|
||||
if surf.GetType() != GeomAbs_Cylinder:
|
||||
return None
|
||||
return float(surf.Cylinder().Radius()) * 2.0
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def feature_info(self, face_id: int) -> dict[str, object]:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
raise ValueError(f"Unknown face id {face_id}")
|
||||
if face_id in self._feature_info_cache:
|
||||
return dict(self._feature_info_cache[face_id])
|
||||
info = self.face_info(face_id)
|
||||
surface = str(info.get("surface", ""))
|
||||
if surface == "cylinder":
|
||||
return self._cylindrical_feature_info(face_id, info)
|
||||
if surface == "plane":
|
||||
return self._planar_feature_info(face_id, info)
|
||||
result = self._cylindrical_feature_info(face_id, info)
|
||||
elif surface == "plane":
|
||||
result = self._planar_feature_info(face_id, info)
|
||||
elif surface == "cone":
|
||||
result = self._conical_feature_info(face_id, info)
|
||||
elif surface == "sphere":
|
||||
result = self._spherical_feature_info(face_id, info)
|
||||
elif surface == "torus":
|
||||
result = self._toroidal_feature_info(face_id, info)
|
||||
else:
|
||||
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
|
||||
result = dict(info)
|
||||
result.update(
|
||||
{
|
||||
"kind": "feature",
|
||||
"feature_type": "暂不支持的局部曲面候选",
|
||||
"feature_source_face_id": face_id,
|
||||
"feature_face_ids": (face_id,),
|
||||
"feature_highlight_face_ids": (face_id,),
|
||||
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
|
||||
"feature_edit_actions": "当前只读;复杂曲面局部面积修改暂未开放",
|
||||
"feature_mode": (
|
||||
"Feature 模式会把选中的Face解释为局部几何特征候选;"
|
||||
"复杂曲面局部面积修改需要更明确的边界/约束重建,当前不会把面积伪装成直接可改参数。"
|
||||
),
|
||||
}
|
||||
)
|
||||
self._feature_info_cache[face_id] = dict(result)
|
||||
return dict(result)
|
||||
|
||||
def _toroidal_feature_info(self, face_id: int, info: dict[str, object]) -> dict[str, object]:
|
||||
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
|
||||
major_radius = float(info.get("major_radius", 0.0) or 0.0)
|
||||
minor_radius = float(info.get("minor_radius", 0.0) or 0.0)
|
||||
result = dict(info)
|
||||
result.update(
|
||||
{
|
||||
"kind": "feature",
|
||||
"feature_type": "暂不支持的局部曲面候选",
|
||||
"feature_type": "环面候选",
|
||||
"feature_source_face_id": face_id,
|
||||
"feature_face_ids": (face_id,),
|
||||
"feature_highlight_face_ids": (face_id,),
|
||||
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
|
||||
"feature_edit_actions": "当前版本只能查看该局部曲面,暂不支持直接编辑。",
|
||||
"feature_mode": "Feature 模式会把选中的Face解释为局部几何特征候选。",
|
||||
"feature_edit_actions": "修改环面主半径/小半径",
|
||||
"feature_mode": (
|
||||
"这是从 STEP/B-Rep 环面直接识别出的几何候选;当前修改会围绕环面中心缩放所属零件/Solid,"
|
||||
"主半径和小半径会等比例变化,不是 CAD 历史里的管径或圆角参数。"
|
||||
),
|
||||
"feature_torus_major_radius": major_radius,
|
||||
"feature_torus_minor_radius": minor_radius,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
def _spherical_feature_info(self, face_id: int, info: dict[str, object]) -> dict[str, object]:
|
||||
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
|
||||
radius = float(info.get("radius", 0.0) or 0.0)
|
||||
result = dict(info)
|
||||
result.update(
|
||||
{
|
||||
"kind": "feature",
|
||||
"feature_type": "球面候选",
|
||||
"feature_source_face_id": face_id,
|
||||
"feature_face_ids": (face_id,),
|
||||
"feature_highlight_face_ids": (face_id,),
|
||||
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
|
||||
"feature_edit_actions": "修改球面半径/直径",
|
||||
"feature_mode": (
|
||||
"这是从 STEP/B-Rep 球面直接识别出的几何候选;修改会围绕球心缩放所属零件/Solid,"
|
||||
"不是 CAD 历史里的球面或圆角参数。"
|
||||
),
|
||||
"feature_sphere_radius": radius,
|
||||
"feature_sphere_diameter": radius * 2.0 if radius > 0 else "",
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
def _conical_feature_info(self, face_id: int, info: dict[str, object]) -> dict[str, object]:
|
||||
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
|
||||
reference_radius = float(info.get("reference_radius", 0.0) or 0.0)
|
||||
result = dict(info)
|
||||
result.update(
|
||||
{
|
||||
"kind": "feature",
|
||||
"feature_type": "圆锥面候选",
|
||||
"feature_source_face_id": face_id,
|
||||
"feature_face_ids": (face_id,),
|
||||
"feature_highlight_face_ids": (face_id,),
|
||||
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
|
||||
"feature_reference_radius": reference_radius,
|
||||
"feature_reference_diameter": reference_radius * 2.0 if reference_radius > 0 else "",
|
||||
"feature_edit_actions": "修改圆锥参考半径/直径",
|
||||
"feature_mode": (
|
||||
"这是从 STEP/B-Rep 圆锥面直接识别出的几何候选;修改会围绕圆锥轴做径向缩放,"
|
||||
"不是 CAD 历史里的锥孔或倒角参数。"
|
||||
),
|
||||
}
|
||||
)
|
||||
return result
|
||||
@@ -549,12 +665,14 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
if guess == "hole/groove candidate":
|
||||
if angular_span < math.tau * 0.92:
|
||||
feature_type = "槽/半孔候选"
|
||||
edit_actions = "调整圆柱孔径;调整槽/半孔宽度;调整槽/半孔深度;调整槽/半孔圆弧长度;调整槽/半孔圆弧角度;调整槽孔总长度"
|
||||
else:
|
||||
feature_type = "圆柱孔候选"
|
||||
edit_actions = "调整圆柱孔径"
|
||||
if info.get("cylinder_end_type") == "blind" and bottom_face_ids:
|
||||
edit_actions = "调整圆柱孔径;调整盲孔深度"
|
||||
edit_actions += ";调整盲孔/盲槽深度"
|
||||
else:
|
||||
edit_actions = "调整圆柱孔径;孔深调整需要明确盲孔底面"
|
||||
edit_actions += ";孔深调整需要明确盲孔底面"
|
||||
if angular_span >= math.tau * 0.92:
|
||||
edit_actions += ";封堵圆柱孔"
|
||||
elif guess == "round/fillet candidate":
|
||||
@@ -562,7 +680,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
edit_actions = "可尝试修改已有圆角半径;当前版本会先移除圆角面,再在恢复出的锐边上重新倒圆。"
|
||||
elif guess == "boss/outer-round candidate":
|
||||
feature_type = "凸台/外圆候选"
|
||||
edit_actions = "调整圆柱凸台直径。"
|
||||
edit_actions = "调整圆柱凸台直径;调整圆柱凸台高度;修改圆柱凸台轴心坐标。"
|
||||
else:
|
||||
feature_type = "未明确圆柱特征"
|
||||
edit_actions = "可尝试调整圆柱孔径,但风险较高。"
|
||||
@@ -704,6 +822,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
|
||||
start_end_face_ids: list[int] = []
|
||||
end_end_face_ids: list[int] = []
|
||||
cap_scan_skipped = False
|
||||
for adjacent_id in adjacent_face_ids:
|
||||
match = self._axis_end_match_for_planar_face(
|
||||
adjacent_id,
|
||||
@@ -720,20 +839,23 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
end_end_face_ids.append(adjacent_id)
|
||||
|
||||
if info.get("cylinder_end_type") == "blind" and (not start_end_face_ids or not end_end_face_ids):
|
||||
scanned = self._axis_cap_face_candidates(
|
||||
face_id,
|
||||
axis_point,
|
||||
axis_dir,
|
||||
v_min,
|
||||
v_max,
|
||||
radius,
|
||||
span,
|
||||
set(adjacent_face_ids),
|
||||
)
|
||||
if not start_end_face_ids:
|
||||
start_end_face_ids.extend(scanned["start"])
|
||||
if not end_end_face_ids:
|
||||
end_end_face_ids.extend(scanned["end"])
|
||||
if len(self.faces) <= 1000:
|
||||
scanned = self._axis_cap_face_candidates(
|
||||
face_id,
|
||||
axis_point,
|
||||
axis_dir,
|
||||
v_min,
|
||||
v_max,
|
||||
radius,
|
||||
span,
|
||||
set(adjacent_face_ids),
|
||||
)
|
||||
if not start_end_face_ids:
|
||||
start_end_face_ids.extend(scanned["start"])
|
||||
if not end_end_face_ids:
|
||||
end_end_face_ids.extend(scanned["end"])
|
||||
else:
|
||||
cap_scan_skipped = True
|
||||
|
||||
bottom_face_ids: list[int] = []
|
||||
opening_face_ids: list[int] = []
|
||||
@@ -755,6 +877,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
bottom_confidence = "medium" if bottom_detection == "axis-cap-scan" else "high"
|
||||
if not end_face_ids:
|
||||
note = "没有在圆柱边界附近找到平面端面。"
|
||||
if cap_scan_skipped:
|
||||
note += " 当前模型 Face 数较多,已跳过全模型底面扫描以避免选择时卡顿;如需改盲孔/盲槽深度,可以手动填写底面 Face ID。"
|
||||
elif bottom_face_ids:
|
||||
if bottom_detection == "axis-cap-scan":
|
||||
note = "已通过轴线端部采样和轴线附近圆盘面扫描标记疑似底面;这是几何推断,不是 CAD 历史孔深。"
|
||||
@@ -762,6 +886,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
note = "已根据圆柱轴线端部 inside/outside 采样标记疑似底面;这是几何推断,不是 CAD 历史孔深。"
|
||||
else:
|
||||
note = "已找到端面候选,但端部采样显示这些端面更像开口附近的相邻面。"
|
||||
if cap_scan_skipped:
|
||||
note += " 当前模型 Face 数较多,已跳过全模型底面扫描以避免选择时卡顿。"
|
||||
return {
|
||||
"end_face_ids": end_face_ids,
|
||||
"start_end_face_ids": sorted(set(start_end_face_ids)),
|
||||
@@ -905,6 +1031,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
for face_id in valid_face_ids:
|
||||
self.face_logical_ids[face_id] = int(logical_id)
|
||||
self._face_info_cache.pop(face_id, None)
|
||||
self._feature_info_cache.pop(face_id, None)
|
||||
|
||||
def nearest_edge_id_to_point(
|
||||
self,
|
||||
@@ -961,9 +1088,17 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
return valid_face_ids[0]
|
||||
|
||||
point_vertex = BRepBuilderAPI_MakeVertex(gp_Pnt(float(point[0]), float(point[1]), float(point[2]))).Vertex()
|
||||
exact_face_ids = valid_face_ids
|
||||
if len(valid_face_ids) > 48:
|
||||
scored_face_ids: list[tuple[float, int]] = []
|
||||
for face_id in valid_face_ids:
|
||||
scored_face_ids.append((self._face_bounds_distance_sq(face_id, point), face_id))
|
||||
scored_face_ids.sort(key=lambda item: item[0])
|
||||
exact_face_ids = [face_id for _distance, face_id in scored_face_ids[:48]]
|
||||
|
||||
best_face_id: int | None = None
|
||||
best_distance = math.inf
|
||||
for face_id in valid_face_ids:
|
||||
for face_id in exact_face_ids:
|
||||
try:
|
||||
extrema = BRepExtrema_DistShapeShape(point_vertex, self.faces[face_id])
|
||||
extrema.Perform()
|
||||
@@ -981,7 +1116,22 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
if distance < best_distance:
|
||||
best_distance = distance
|
||||
best_face_id = face_id
|
||||
return best_face_id if best_face_id is not None else valid_face_ids[0]
|
||||
return best_face_id if best_face_id is not None else exact_face_ids[0]
|
||||
|
||||
def _face_bounds_distance_sq(self, face_id: int, point: tuple[float, float, float]) -> float:
|
||||
px, py, pz = (float(point[0]), float(point[1]), float(point[2]))
|
||||
try:
|
||||
xmin, ymin, zmin, xmax, ymax, zmax = _shape_bounds(self.faces[face_id])
|
||||
dx = max(xmin - px, 0.0, px - xmax)
|
||||
dy = max(ymin - py, 0.0, py - ymax)
|
||||
dz = max(zmin - pz, 0.0, pz - zmax)
|
||||
return dx * dx + dy * dy + dz * dz
|
||||
except Exception:
|
||||
try:
|
||||
center = _surface_center(self.faces[face_id])
|
||||
return _point_distance_sq((px, py, pz), _point_tuple(center))
|
||||
except Exception:
|
||||
return math.inf
|
||||
|
||||
def _adjacent_face_ids_for_edges(self, edge_ids: list[int], face_id: int) -> list[int]:
|
||||
if not edge_ids:
|
||||
|
||||
Reference in New Issue
Block a user