feat: 完善参数化编辑和孔修改稳定性
This commit is contained in:
+100
-14
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user