feat: 完善参数化编辑和孔修改稳定性

This commit is contained in:
2026-07-28 18:30:58 +08:00
parent 7c263d1e96
commit 720c2dc970
13 changed files with 2190 additions and 690 deletions
+100 -14
View File
@@ -106,18 +106,18 @@ class OperationMixin:
def repair_part(self, part_id: int) -> str:
part = self.part_by_id(part_id)
if part is None:
raise ValueError(f"Unknown part id {part_id}")
raise ValueError(f"未知零件 ID {part_id}")
if part.shape.IsNull():
raise RuntimeError(f"Part {part_id} has a null shape and cannot be repaired.")
raise RuntimeError(f"零件 {part_id} 为空 shape,无法修复。")
before_stats = self.part_topology_stats(part_id)
repaired = _prepare_shape_for_step_export(part.shape)
if repaired.IsNull():
raise RuntimeError(f"Part {part_id} repair returned a null shape.")
raise RuntimeError(f"零件 {part_id} 修复结果为空 shape")
part.shape = repaired
self.refresh_topology()
after_stats = self.part_topology_stats(part_id)
return (
f"Part repair completed: part {part_id}, "
f"零件修复完成: 零件 {part_id}, "
f"solids {before_stats.solids}->{after_stats.solids}, "
f"faces {before_stats.faces}->{after_stats.faces}, "
f"edges {before_stats.edges}->{after_stats.edges}."
@@ -180,7 +180,7 @@ class OperationMixin:
readiness["fillet_risk"] = _max_risk(str(readiness["fillet_risk"]), "high")
readiness["fillet_warnings"] = _join_nonempty(
readiness["fillet_warnings"],
f"当前零件包含 {part_stats.solids} 个Solid,Edge倒圆会作用在整个Part shape 上,请导出前检查结果。",
f"当前零件包含 {part_stats.solids} 个Solid,Edge倒圆会作用在整个零件 shape 上,请导出前检查结果。",
)
readiness["fillet_note"] = _join_nonempty(readiness["fillet_note"], readiness["fillet_warnings"])
@@ -224,7 +224,7 @@ class OperationMixin:
readiness["chamfer_risk"] = _max_risk(str(readiness["chamfer_risk"]), "high")
readiness["chamfer_warnings"] = _join_nonempty(
readiness["chamfer_warnings"],
f"当前零件包含 {part_stats.solids} 个Solid,Edge倒角会作用在整个Part shape 上,请导出前检查结果。",
f"当前零件包含 {part_stats.solids} 个Solid,Edge倒角会作用在整个零件 shape 上,请导出前检查结果。",
)
readiness["chamfer_note"] = _join_nonempty(readiness["chamfer_note"], readiness["chamfer_warnings"])
@@ -479,7 +479,7 @@ class OperationMixin:
part_id = int(edge_info.get("part_id", -1))
part = self.part_by_id(part_id)
if part is None:
return None, "局部边形变不可用:找不到所属Part"
return None, "局部边形变不可用:找不到所属零件"
solid = self.solids[solid_id][1]
solid_faces = _explore(solid, TopAbs_FACE)
if not solid_faces:
@@ -760,7 +760,7 @@ class OperationMixin:
solid_id = int(edge_info.get("solid_id", -1))
part = self.part_by_id(part_id) if part_id >= 0 else None
if part is None:
return None, "平面曲线径向缩放不可用:找不到所属Part"
return None, "平面曲线径向缩放不可用:找不到所属零件"
part_solid_count = len(_explore(part.shape, TopAbs_SOLID))
target_kind = "solid" if solid_id >= 0 and part_solid_count > 1 else "part"
scale = float(target_length) / max(current_length, 1e-9)
@@ -1634,7 +1634,7 @@ class OperationMixin:
or ("solid" if solid_id >= 0 else "part")
)
edge_ids = [edge_id for edge_id in range(len(self.edges)) if part_id < 0 or self.edge_part_ids[edge_id] == part_id]
scope = f"Part {part_id}" if part_id >= 0 else "model"
scope = f"零件 {part_id}" if part_id >= 0 else "model"
if target_kind == "solid" and solid_id >= 0:
solid_edge_ids = [edge_id for edge_id in edge_ids if self.edge_solid_ids[edge_id] == solid_id]
if solid_edge_ids:
@@ -2246,6 +2246,8 @@ class OperationMixin:
return None
if face_id < 0 or face_id >= len(self.faces):
return None
if len(_explore(self.faces[face_id], TopAbs_WIRE)) > 1:
return None
cap_center = _surface_center(self.faces[face_id])
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
@@ -2256,7 +2258,7 @@ class OperationMixin:
diagonal = _shape_diagonal(self.shape)
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
outward_dir = gp_Dir(float(outward[0]), float(outward[1]), float(outward[2]))
best: tuple[float, dict[str, object]] | None = None
candidates: list[tuple[float, dict[str, object]]] = []
for adjacent_id in adjacent_face_ids:
side_surf = BRepAdaptor_Surface(self.faces[adjacent_id])
@@ -2312,10 +2314,18 @@ class OperationMixin:
"start_parameter": new_min,
"end_parameter": new_max,
}
if best is None or score < best[0]:
best = (score, candidate)
candidates.append((score, candidate))
return best[1] if best is not None else None
if not candidates:
return None
distinct_radii: list[float] = []
for _score, candidate in candidates:
radius = float(candidate["radius"])
if not any(abs(radius - existing) <= max(radius, existing, 1.0) * 1e-5 for existing in distinct_radii):
distinct_radii.append(radius)
if len(distinct_radii) > 1:
return None
return min(candidates, key=lambda item: item[0])[1]
def resize_existing_fillet(self, face_id: int, target_radius: float) -> str:
plan = self.existing_fillet_resize_plan(face_id, target_radius)
@@ -2441,6 +2451,7 @@ class OperationMixin:
raise ValueError(f"Unknown part id {part_id}")
direction = cyl.Axis().Direction()
old_part_shape = part.shape
source_shape = part.shape
if plan["resize_mode"] == "shrink":
fill_start = gp_Pnt(*plan["fill_start_point"])
@@ -2461,14 +2472,89 @@ class OperationMixin:
result = _finalize_boolean_result(op, "cylinder cut")
part.shape = result
self.refresh_topology()
verification = self._verify_cylindrical_resize_result(plan, part_id)
if not verification["matched"]:
part.shape = old_part_shape
self.refresh_topology()
detail = str(verification.get("detail", ""))
raise RuntimeError(
"孔/圆柱直径布尔计算返回了结果,但结果里没有检测到目标直径的圆柱面,"
"已回滚到修改前状态。"
f"{detail}"
)
action = "enlarged by bounded cut" if plan["resize_mode"] == "enlarge" else "shrunk by fill and recut"
return (
f"Cylindrical resize completed: diameter {old_radius * 2.0:g} -> {new_diameter:g}, "
f"mode={plan['resize_mode']}, action={action}, "
f"risk={plan['risk']}, feature={plan['feature_guess']}, "
f"cutter={plan['cutter_strategy']}, height={float(plan['cutter_height']):g}."
f"cutter={plan['cutter_strategy']}, height={float(plan['cutter_height']):g}, "
f"verified_face={verification.get('face_id', '')}."
)
def _verify_cylindrical_resize_result(self, plan: dict[str, object], part_id: int) -> dict[str, object]:
target_diameter = float(plan.get("target_diameter", 0.0))
target_radius = target_diameter / 2.0
if target_radius <= 1e-9:
return {"matched": False, "detail": " 目标直径无效。"}
try:
axis_point = gp_Pnt(*plan["cutter_axis_point"])
axis_direction = gp_Dir(*plan["cutter_axis_direction"])
except Exception:
return {"matched": False, "detail": " 缺少原孔轴信息,无法确认结果。"}
part = self.part_by_id(part_id)
diagonal = max(_shape_diagonal(part.shape) if part is not None else 0.0, target_radius, 1.0)
radius_tolerance = max(target_radius * 0.02, diagonal * 1e-6, 1e-5)
axis_tolerance = max(target_radius * 0.08, diagonal * 1e-5, 1e-4)
best: dict[str, object] | None = None
best_score = math.inf
for face_id, face in enumerate(self.faces):
if self.face_part_ids[face_id] != part_id:
continue
try:
surf = BRepAdaptor_Surface(face)
if surf.GetType() != GeomAbs_Cylinder:
continue
cyl = surf.Cylinder()
candidate_radius = float(cyl.Radius())
radius_error = abs(candidate_radius - target_radius)
axis_dot = abs(_direction_dot(axis_direction, cyl.Axis().Direction()))
if axis_dot < 1.0 - 1e-5:
continue
axis_distance = _point_axis_distance(axis_point, axis_direction, cyl.Axis().Location())
except Exception:
continue
score = radius_error / max(radius_tolerance, 1e-9) + axis_distance / max(axis_tolerance, 1e-9)
candidate = {
"matched": radius_error <= radius_tolerance and axis_distance <= axis_tolerance,
"face_id": face_id,
"diameter": candidate_radius * 2.0,
"radius_error": radius_error,
"axis_distance": axis_distance,
"radius_tolerance": radius_tolerance,
"axis_tolerance": axis_tolerance,
}
if candidate["matched"]:
return candidate
if score < best_score:
best_score = score
best = candidate
if best is None:
return {"matched": False, "detail": " 未找到同零件内与原孔轴平行的圆柱面。"}
return {
"matched": False,
"detail": (
f" 最近候选 Face {best['face_id']} 的直径约 {float(best['diameter']):.6g}"
f"目标直径 {target_diameter:.6g}"
f"半径误差 {float(best['radius_error']):.6g}"
f"轴距 {float(best['axis_distance']):.6g}"
),
**best,
}
def resize_cylindrical_boss(self, face_id: int, new_diameter: float) -> str:
plan = self.cylindrical_boss_resize_plan(face_id, new_diameter)
if plan["status"] == "blocked":