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
+637 -17
View File
@@ -20,6 +20,15 @@ from PySide6.QtWidgets import (
from .model import StepModel
from .records import OperationRecord
from .geometry_utils import (
_tuple_add,
_tuple_dot,
_tuple_normalized,
_tuple_or_none,
_tuple_scale,
_tuple_sub,
_vector_length,
)
from .ui_helpers import * # noqa: F403
from .workers import EditWorker, LoadWorker, ScanWorker
@@ -63,6 +72,11 @@ def _unit_triple_or_none(value: object) -> tuple[float, float, float] | None:
return (triple[0] / length, triple[1] / length, triple[2] / length)
def _compact_plan_value(value: object) -> str:
text = _format_value(value)
return text if len(text) <= 120 else text[:117] + "..."
class WindowActionMixin:
def export_all(self) -> None:
if self.model is None:
@@ -339,7 +353,7 @@ class WindowActionMixin:
QMessageBox.critical(self, "距离无效", "请输入数字形式的面移动距离。")
return
face_id = self.selected_face_id
plan = self._quick_push_pull_plan(face_id, distance)
plan = self._push_pull_plan_for_action(face_id, distance)
if plan["status"] == "blocked":
QMessageBox.information(self, "不能推拉平面", str(plan["message"]))
@@ -411,6 +425,24 @@ class WindowActionMixin:
"push_pull_inward_material_depth": plan.get("push_pull_inward_material_depth"),
"push_pull_inward_cut_ratio": plan.get("push_pull_inward_cut_ratio"),
"bbox_diagonal": plan.get("bbox_diagonal"),
"selected_boundary_wires": plan.get("selected_boundary_wires"),
"selected_inner_boundary_wires": plan.get("selected_inner_boundary_wires"),
"selected_has_inner_boundaries": plan.get("selected_has_inner_boundaries"),
"same_domain_face_count": plan.get("same_domain_face_count"),
"first_level_boundary_edge_count": plan.get("first_level_boundary_edge_count"),
"first_level_boundary_vertex_count": plan.get("first_level_boundary_vertex_count"),
"first_level_adjacent_face_count": plan.get("first_level_adjacent_face_count"),
"topology_relation_status": plan.get("topology_relation_status"),
"topology_ignored_relation_note": plan.get("topology_ignored_relation_note"),
"first_level_topology_note": plan.get("first_level_topology_note"),
"planar_cap_extension_kind": plan.get("planar_cap_extension_kind"),
"planar_cap_extension_method": plan.get("planar_cap_extension_method"),
"planar_cap_boundary_edge_count": plan.get("planar_cap_boundary_edge_count"),
"planar_cap_inner_boundary_wires": plan.get("planar_cap_inner_boundary_wires"),
"planar_cap_adjacent_face_count": plan.get("planar_cap_adjacent_face_count"),
"cylindrical_cap_extension_kind": plan.get("cylindrical_cap_extension_kind"),
"cylindrical_cap_extension_method": plan.get("cylindrical_cap_extension_method"),
"cap_extra_adjacent_face_count": plan.get("cap_extra_adjacent_face_count"),
},
target_kind="face",
target_id=face_id,
@@ -495,6 +527,20 @@ class WindowActionMixin:
if risk == "low":
risk = "medium"
warnings.append("向内推拉的材料厚度尚未缓存;完整切穿检查会放到后台计算。")
model_face_count = len(self.model.faces) if self.model is not None else 0
inner_wires = int(info.get("inner_boundary_wires", 0) or 0)
if status != "blocked" and model_face_count > 600 and inner_wires > 1:
status = "blocked"
risk = "blocked"
boundary_edges = _compact_plan_value(info.get("first_level_boundary_edge_count", "未知"))
adjacent_faces = _compact_plan_value(info.get("first_level_adjacent_face_count", "未知"))
blockers.append(
f"当前 Face {face_id} 是复杂大模型里的多内孔/多边界平面端盖:"
f"内边界 {inner_wires} 个,一级边界 Edge {boundary_edges} 条,"
f"共享边一级相邻 Face {adjacent_faces} 个。向内推拉这类面需要判断孔壁、"
"槽底或台阶背后的二级关系是否一起变化;当前阶段只自动处理一级关系。"
"已在界面预检查阶段阻止,避免进入通用 OCCT 布尔后长时间卡住。"
)
if status != "blocked" and risk in {"medium", "high"}:
status = "caution"
@@ -538,6 +584,10 @@ class WindowActionMixin:
"surface": surface,
"area": info.get("area"),
"bbox_diagonal": bbox_diagonal,
"boundary_wires": info.get("boundary_wires"),
"inner_boundary_wires": info.get("inner_boundary_wires"),
"has_inner_boundaries": bool(info.get("has_inner_boundaries")),
"model_face_count": len(self.model.faces) if self.model is not None else None,
"push_pull_inward_material_depth": inward_material_depth,
"push_pull_inward_cut_ratio": inward_cut_ratio,
"outward_direction": outward_tuple,
@@ -554,8 +604,104 @@ class WindowActionMixin:
"push_pull_scope_face_ids": tuple(scope_face_ids),
"push_pull_scope_face_count": len(scope_face_ids),
"push_pull_scope_note": "使用当前显示/选中缓存生成快速预览;后台会重新计算真实推拉区域。",
"selected_boundary_wires": info.get("boundary_wires"),
"selected_inner_boundary_wires": info.get("inner_boundary_wires"),
"selected_has_inner_boundaries": bool(info.get("has_inner_boundaries")),
"same_domain_face_count": info.get("same_domain_face_count"),
"first_level_boundary_edge_count": info.get("first_level_boundary_edge_count"),
"first_level_boundary_vertex_count": info.get("first_level_boundary_vertex_count"),
"first_level_adjacent_face_count": info.get("first_level_adjacent_face_count"),
"topology_relation_status": info.get("topology_relation_status"),
"topology_ignored_relation_note": info.get("topology_ignored_relation_note"),
"first_level_topology_note": info.get("first_level_topology_note"),
"ui_quick_blocked_push_pull_plan": bool(
status == "blocked"
and distance < 0
and (len(self.model.faces) if self.model is not None else 0) > 600
and int(info.get("inner_boundary_wires", 0) or 0) > 1
),
}
def _should_defer_push_pull_model_plan(
self,
face_id: int,
distance: float,
quick_plan: dict[str, object],
) -> bool:
if self.model is None or quick_plan.get("status") == "blocked":
return False
if str(quick_plan.get("surface", "")) != "plane":
return False
try:
model_face_count = len(self.model.faces)
except Exception:
model_face_count = 0
if model_face_count < 600:
return False
inner_wires = int(quick_plan.get("inner_boundary_wires") or 0)
boundary_wires = int(quick_plan.get("boundary_wires") or 0)
if inner_wires <= 0 and boundary_wires <= 1 and not bool(quick_plan.get("has_inner_boundaries")):
return False
# Large STEP + holed planar caps are exactly where a full plan can spend
# seconds scanning topology before the actual isolated edit even starts.
return abs(float(distance)) > 1e-9
def _deferred_push_pull_model_plan(
self,
quick_plan: dict[str, object],
) -> dict[str, object]:
plan = dict(quick_plan)
warnings = [item for item in str(plan.get("warnings") or "").split("") if item]
warnings.append(
"当前是复杂大模型里的多边界平面;完整几何计划将放到后台/隔离子进程里计算,避免主界面先卡住。"
)
plan["warnings"] = "".join(warnings)
plan["risk"] = self._max_quick_risk(str(plan.get("risk") or "low"), "high")
if plan.get("status") != "blocked":
plan["status"] = "caution"
plan["ui_deferred_model_plan"] = True
plan["quick_plan_status"] = quick_plan.get("status")
plan["quick_plan_risk"] = quick_plan.get("risk")
plan["message"] = (
"为避免复杂 STEP 在界面线程生成完整推拉计划时卡顿,本次只做快速预检查;"
"真正的一级关系识别、风险判断、解析重建或快速阻止会在后台隔离进程中完成。"
)
plan.setdefault("edit_strategy_label", "后台计算完整推拉计划")
plan.setdefault(
"edit_semantics",
"界面先提交后台任务;子进程会按当前 Face 的一级拓扑关系决定是解析重建、局部重建还是阻止。",
)
return plan
def _push_pull_plan_for_action(self, face_id: int, distance: float) -> dict[str, object]:
quick_plan = self._quick_push_pull_plan(face_id, distance)
if quick_plan.get("status") == "blocked":
return quick_plan
if self.model is None:
return quick_plan
if self._should_defer_push_pull_model_plan(face_id, distance, quick_plan):
return self._deferred_push_pull_model_plan(quick_plan)
try:
model_plan = self.model.push_pull_plan(face_id, distance)
except Exception as exc:
blocked_plan = dict(quick_plan)
blocked_plan.update(
{
"status": "blocked",
"risk": "blocked",
"message": f"无法生成完整推拉计划:{exc}",
"model_plan_error": str(exc),
"quick_plan_status": quick_plan.get("status"),
"quick_plan_risk": quick_plan.get("risk"),
}
)
return blocked_plan
model_plan.setdefault("quick_plan_status", quick_plan.get("status"))
model_plan.setdefault("quick_plan_risk", quick_plan.get("risk"))
return model_plan
def resize_shell_thickness(self) -> None:
if self.model is None:
return
@@ -573,7 +719,9 @@ class WindowActionMixin:
return
face_id = self.selected_face_id
plan = self.model.shell_thickness_plan(face_id, target_thickness)
plan = self._deferred_shell_thickness_plan_if_needed(face_id, target_thickness)
if plan is None:
plan = self.model.shell_thickness_plan(face_id, target_thickness)
if plan["status"] == "blocked":
QMessageBox.information(self, "不能调整薄壁厚度", str(plan["message"]))
self.statusBar().showMessage("薄壁厚度调整已阻止")
@@ -782,6 +930,11 @@ class WindowActionMixin:
info = {}
if info and str(info.get("kind", "")) in {"edge", "part", "solid"} and not has_selected_id:
info = {}
if "surface" not in info and "diameter" not in info:
try:
info = dict(self.model.quick_face_info(selected_face_id))
except Exception:
pass
if "surface" not in info and "diameter" not in info:
try:
info = dict(self.model.face_info(selected_face_id))
@@ -789,6 +942,286 @@ class WindowActionMixin:
pass
return info
def _large_complex_local_face_blocker(self, face_id: int, info: dict[str, object]) -> str:
if self.model is None:
return ""
try:
model_face_count = len(self.model.faces)
except Exception:
model_face_count = 0
if model_face_count < 600:
return ""
if str(info.get("surface", "")) != "plane":
return ""
try:
inner_wires = int(info.get("inner_boundary_wires") or 0)
boundary_wires = int(info.get("boundary_wires") or 0)
except (TypeError, ValueError):
inner_wires = 0
boundary_wires = 0
local_ready = info.get("local_face_deform_ready")
if inner_wires <= 0 and boundary_wires <= 1 and local_ready is not False:
return ""
base = str(info.get("local_face_deform_blocker") or "").strip()
if not base:
base = "当前 Face 位于复杂大 STEP 中,并且有内孔、内边界或复杂边界;不适合做只改当前 Face 的局部变形。"
return (
f"{base} 为避免在界面线程生成完整局部重建计划时卡顿,已在轻量预检查阶段阻止;"
"请改用 `推拉当前面`、孔/槽专门入口、`移动整个特征` 或 `调整整个特征`。"
)
def _quick_blocked_local_face_plan(
self,
face_id: int,
blocker: str,
*,
resize_strategy: str,
edit_strategy_label: str,
edit_semantics: str,
info: dict[str, object] | None = None,
extra: dict[str, object] | None = None,
) -> dict[str, object]:
source = dict(info or self._selected_face_info_snapshot())
plan = {
"status": "blocked",
"risk": "blocked",
"message": blocker,
"warnings": "",
"blockers": blocker,
"face_id": face_id,
"part_id": source.get("part_id"),
"solid_id": source.get("solid_id"),
"surface": source.get("surface"),
"area": source.get("area"),
"area_center": source.get("area_center"),
"bbox_diagonal": source.get("bbox_diagonal"),
"boundary_wires": source.get("boundary_wires"),
"inner_boundary_wires": source.get("inner_boundary_wires"),
"has_inner_boundaries": bool(source.get("has_inner_boundaries")),
"local_face_deform_ready": source.get("local_face_deform_ready"),
"local_face_deform_blocker": source.get("local_face_deform_blocker"),
"local_face_deform_face_count": source.get("local_face_deform_face_count", 1),
"local_face_deform_moved_point_count": 0,
"local_face_deform_target_kind": "blocked-quick-preflight",
"resize_strategy": resize_strategy,
"edit_strategy_label": edit_strategy_label,
"edit_semantics": edit_semantics,
"quick_preflight": True,
"ui_quick_blocked_local_face_plan": True,
}
if extra:
plan.update(extra)
return plan
def _quick_blocked_face_area_local_plan(self, face_id: int, target_area: float) -> dict[str, object] | None:
info = self._selected_face_info_snapshot()
blocker = self._large_complex_local_face_blocker(face_id, info)
if not blocker:
return None
current_area = _float_or_none(info.get("area"))
area_delta = target_area - current_area if current_area is not None else None
area_delta_ratio = abs(area_delta) / current_area if current_area is not None and current_area > 1e-9 else None
area_scale = math.sqrt(target_area / current_area) if current_area is not None and current_area > 1e-9 and target_area > 0 else None
return self._quick_blocked_local_face_plan(
face_id,
blocker,
resize_strategy="local-face-area-only-deform",
edit_strategy_label="只缩放当前Face面积",
edit_semantics="只移动当前 Face 的边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
info=info,
extra={
"current_area": current_area,
"target_area": target_area,
"area_delta": area_delta,
"area_delta_ratio": area_delta_ratio,
"local_face_area_scale": area_scale,
},
)
def _quick_blocked_face_size_local_plan(
self,
face_id: int,
target_size: float,
axis_key: str,
) -> dict[str, object] | None:
info = self._selected_face_info_snapshot()
blocker = self._large_complex_local_face_blocker(face_id, info)
if not blocker:
return None
current_width = _float_or_none(info.get("local_face_width"))
current_height = _float_or_none(info.get("local_face_height"))
current_size = current_height if axis_key == "height" else current_width
delta = target_size - current_size if current_size is not None else None
ratio = abs(delta) / current_size if current_size is not None and current_size > 1e-9 else None
scale = target_size / current_size if current_size is not None and current_size > 1e-9 else None
axis_label = "面高" if axis_key == "height" else "面宽"
return self._quick_blocked_local_face_plan(
face_id,
blocker,
resize_strategy=f"local-face-{axis_key}-only-deform",
edit_strategy_label=f"{axis_label}(当前面)",
edit_semantics="只沿当前 Face 的一个面内方向移动边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
info=info,
extra={
"face_size_axis": axis_key,
"face_size_label": axis_label,
"current_face_width": current_width,
"target_face_width": target_size if axis_key == "width" else current_width,
"current_face_height": current_height,
"target_face_height": target_size if axis_key == "height" else current_height,
"current_face_size": current_size,
"target_face_size": target_size,
"face_size_delta": delta,
"face_size_delta_ratio": ratio,
"face_size_scale": scale,
"face_size_center": info.get("area_center") or info.get("bbox_center"),
"face_size_axis_direction": info.get("face_height_direction" if axis_key == "height" else "face_width_direction"),
"face_width_direction": info.get("face_width_direction"),
"face_height_direction": info.get("face_height_direction"),
},
)
def _quick_blocked_face_center_local_plan(
self,
face_id: int,
target_center: tuple[float, float, float],
) -> dict[str, object] | None:
info = self._selected_face_info_snapshot()
blocker = self._large_complex_local_face_blocker(face_id, info)
if not blocker:
return None
current_center = _tuple_or_none(info.get("area_center")) or _tuple_or_none(info.get("bbox_center"))
move_vector = _tuple_sub(target_center, current_center) if current_center is not None else None
move_distance = _vector_length(move_vector) if move_vector is not None else None
bbox_diagonal = _float_or_none(info.get("bbox_diagonal"))
move_ratio = move_distance / bbox_diagonal if move_distance is not None and bbox_diagonal is not None and bbox_diagonal > 1e-9 else None
return self._quick_blocked_local_face_plan(
face_id,
blocker,
resize_strategy="local-face-only-deform",
edit_strategy_label="只移动当前Face",
edit_semantics="只移动当前 Face 的边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
info=info,
extra={
"current_face_center": current_center,
"target_face_center": target_center,
"face_center_move_vector": move_vector,
"face_center_move_distance": move_distance,
"face_center_move_ratio": move_ratio,
},
)
def _quick_blocked_face_plane_offset_local_plan(self, face_id: int, distance: float) -> dict[str, object] | None:
info = self._selected_face_info_snapshot()
blocker = self._large_complex_local_face_blocker(face_id, info)
if not blocker:
return None
plane_origin = _tuple_or_none(info.get("plane_origin"))
plane_direction = (
_tuple_normalized(_tuple_or_none(info.get("push_pull_outward_direction")))
or _tuple_normalized(_tuple_or_none(info.get("oriented_normal")))
or _tuple_normalized(_tuple_or_none(info.get("normal")))
)
current_position = _tuple_dot(plane_origin, plane_direction) if plane_origin is not None and plane_direction is not None else None
target_position = current_position + distance if current_position is not None else None
current_center = _tuple_or_none(info.get("area_center")) or _tuple_or_none(info.get("bbox_center"))
move_vector = _tuple_scale(plane_direction, distance) if plane_direction is not None else None
target_center = _tuple_add(current_center, move_vector) if current_center is not None and move_vector is not None else None
move_distance = abs(float(distance))
bbox_diagonal = _float_or_none(info.get("bbox_diagonal"))
move_ratio = move_distance / bbox_diagonal if bbox_diagonal is not None and bbox_diagonal > 1e-9 else None
return self._quick_blocked_local_face_plan(
face_id,
blocker,
resize_strategy="local-face-plane-offset-deform",
edit_strategy_label="面偏移(当前面)",
edit_semantics="按当前面垂直方向移动 Face 边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
info=info,
extra={
"current_plane_position": current_position,
"target_plane_position": target_position,
"plane_origin": plane_origin,
"plane_direction": plane_direction,
"plane_offset_distance": distance,
"current_face_center": current_center,
"target_face_center": target_center,
"face_center_move_vector": move_vector,
"face_center_move_distance": move_distance,
"face_center_move_ratio": move_ratio,
},
)
def _deferred_shell_thickness_plan_if_needed(
self,
face_id: int,
target_thickness: float,
) -> dict[str, object] | None:
info = self._selected_face_info_snapshot()
blocker = self._large_complex_local_face_blocker(face_id, info)
if not blocker:
return None
current = (
_float_or_none(info.get("shell_thickness_estimate"))
or _float_or_none(info.get("shell_current_thickness"))
or _float_or_none(info.get("shell_signed_thickness"))
)
if target_thickness <= 0:
return self._quick_blocked_local_face_plan(
face_id,
"目标薄壁厚度必须大于 0。",
resize_strategy="push-pull-shell-source-plane-to-target-thickness",
edit_strategy_label="薄壁厚度(当前面)",
edit_semantics="移动当前平面区域以接近目标薄壁厚度。",
info=info,
extra={
"shell_current_thickness": current,
"shell_target_thickness": target_thickness,
"shell_delta_thickness": None,
"shell_delta_ratio": None,
},
)
delta = target_thickness - current if current is not None else None
ratio = abs(delta) / current if current is not None and current > 1e-9 else None
warnings = [
"复杂大 STEP 的薄壁相对面识别会放到后台隔离进程里执行,避免主界面先卡住。",
"执行前请确认这是想修改的局部薄壁区域,而不是孔/槽或台阶端面。",
]
return {
"status": "caution",
"risk": "high",
"message": (
"为避免在界面线程扫描复杂模型里的相对面,本次只做快速预检查;"
"真正的薄壁相对面识别、推拉距离计算和结果校验会在后台隔离进程中完成。"
),
"warnings": "".join(warnings),
"blockers": "",
"face_id": face_id,
"part_id": info.get("part_id"),
"solid_id": info.get("solid_id"),
"surface": info.get("surface"),
"shell_current_thickness": current,
"shell_target_thickness": target_thickness,
"shell_delta_thickness": delta,
"shell_delta_ratio": ratio,
"shell_signed_thickness": info.get("shell_signed_thickness"),
"shell_opposite_face_id": info.get("shell_opposite_face_id"),
"shell_overlap_ratio_estimate": info.get("shell_overlap_ratio_estimate"),
"shell_confidence": info.get("shell_confidence", "deferred"),
"shell_source_face_ids": info.get("shell_source_face_ids", (face_id,)),
"shell_region_kind": info.get("shell_region_kind", "deferred-complex-face"),
"push_pull_distance": None,
"outward_direction": info.get("push_pull_outward_direction") or info.get("oriented_normal") or info.get("normal"),
"push_pull_scope_face_ids": info.get("push_pull_scope_face_ids") or (face_id,),
"push_pull_scope_face_count": len(_int_values(info.get("push_pull_scope_face_ids")) or [face_id]),
"push_pull_scope_note": "复杂薄壁计划延后到后台隔离进程重新计算。",
"resize_strategy": "push-pull-shell-source-plane-to-target-thickness",
"edit_strategy_label": "薄壁厚度(当前面)",
"edit_semantics": "后台识别相对平面后,把当前平面区域推拉到目标薄壁厚度;失败会保持原模型不变。",
"quick_preflight": True,
"ui_deferred_model_plan": True,
"ui_deferred_shell_thickness_plan": True,
}
def _quick_cylinder_resize_plan(
self,
face_id: int,
@@ -982,6 +1415,8 @@ class WindowActionMixin:
"面高(整体)",
"面偏移(当前面)",
"只移动当前Face",
"圆柱高度调整",
"高度(整体)缩放所属对象",
}
def _isolation_for_plan(
@@ -1003,6 +1438,9 @@ class WindowActionMixin:
"move_face_center_local",
"resize_shell_thickness",
"resize_shell_thickness_owning_scale",
"resize_cylindrical_height",
"resize_cylindrical_boss_height",
"resize_cylindrical_height_owning_scale",
"resize_cone_reference_radius",
"resize_cone_semi_angle",
"resize_sphere_radius",
@@ -1019,6 +1457,108 @@ class WindowActionMixin:
"reason": f"{risk}-risk-face-occ-edit",
}
def _edit_failure_diagnostics(self, context: dict[str, object]) -> str:
parameters = context.get("parameters")
if not isinstance(parameters, dict):
parameters = {}
lines: list[str] = []
operation_name = str(context.get("operation_name") or "").strip()
target = str(context.get("target") or "").strip()
if operation_name or target:
lines.append(f"操作: {operation_name or '未知'} / {target or '未知对象'}")
strategy = str(parameters.get("edit_strategy_label") or "").strip()
if strategy:
lines.append(f"编辑策略: {strategy}")
surface = parameters.get("surface")
distance = parameters.get("semantic_distance")
current_position = parameters.get("current_plane_position")
target_position = parameters.get("target_plane_position")
surface_chunks: list[str] = []
if surface not in {None, ""}:
surface_chunks.append(f"面类型={_compact_plan_value(surface)}")
if distance not in {None, ""}:
surface_chunks.append(f"修改量={_compact_plan_value(distance)}")
if current_position not in {None, ""}:
surface_chunks.append(f"当前位置={_compact_plan_value(current_position)}")
if target_position not in {None, ""}:
surface_chunks.append(f"目标位置={_compact_plan_value(target_position)}")
if surface_chunks:
lines.append("当前对象: " + "".join(surface_chunks))
boundary_chunks: list[str] = []
boundary_keys = (
("线圈", "selected_boundary_wires"),
("内孔/内边界", "selected_inner_boundary_wires"),
("同域Face", "same_domain_face_count"),
("一级Edge", "first_level_boundary_edge_count"),
("一级Vertex", "first_level_boundary_vertex_count"),
("一级相邻Face", "first_level_adjacent_face_count"),
)
for label, key in boundary_keys:
value = parameters.get(key)
if value not in {None, ""}:
boundary_chunks.append(f"{label}={_compact_plan_value(value)}")
if boundary_chunks:
lines.append("一级关系证据: " + "".join(boundary_chunks))
topology_note = str(parameters.get("first_level_topology_note") or "").strip()
ignored_note = str(parameters.get("topology_ignored_relation_note") or "").strip()
if topology_note:
lines.append(f"一级关系说明: {topology_note}")
if ignored_note:
lines.append(f"暂不处理范围: {ignored_note}")
planar_method = str(parameters.get("planar_cap_extension_method") or "").strip()
cylindrical_method = str(parameters.get("cylindrical_cap_extension_method") or "").strip()
if planar_method or cylindrical_method:
method_parts: list[str] = []
if planar_method:
method_parts.append(
f"平面端盖={_compact_plan_value(parameters.get('planar_cap_extension_kind'))}"
f"/{_compact_plan_value(planar_method)}"
)
if cylindrical_method:
method_parts.append(
f"圆柱端盖={_compact_plan_value(parameters.get('cylindrical_cap_extension_kind'))}"
f"/{_compact_plan_value(cylindrical_method)}"
)
lines.append("已识别的专用路径: " + "".join(method_parts))
risk_message = str(parameters.get("push_pull_message") or "").strip()
if risk_message:
lines.append(f"计划阶段判断: {risk_message}")
isolation = context.get("isolation")
if isinstance(isolation, dict) and isolation:
timeout = isolation.get("timeout_seconds")
reason = str(isolation.get("reason") or "").strip()
if timeout not in {None, ""}:
lines.append(f"隔离保护: 子进程超时上限 {_compact_plan_value(timeout)}s")
if reason:
lines.append(f"隔离原因: {reason}")
has_inner_boundaries = bool(parameters.get("selected_has_inner_boundaries"))
try:
inner_wires = int(parameters.get("selected_inner_boundary_wires", 0) or 0)
except (TypeError, ValueError):
inner_wires = 0
if has_inner_boundaries or inner_wires > 0:
lines.append(
"可能原因: 当前面带孔/内边界。若修改方向会越过孔壁、槽底、台阶终点,"
"就可能涉及一级相邻面背后的二级或更深拓扑关系;当前阶段只自动传播一级关系。"
)
elif str(parameters.get("push_pull_risk") or "") == "high":
lines.append(
"可能原因: 当前操作风险较高,底层 OCCT 布尔或局部重建可能返回无效 B-Rep;"
"这通常不是界面卡住,而是几何内核没有稳定给出可用结果。"
)
if not lines:
return ""
return "\n\n诊断信息:\n" + "\n".join(f"- {line}" for line in lines)
def resize_hole(self) -> None:
if self.model is None:
return
@@ -2413,7 +2953,7 @@ class WindowActionMixin:
self.statusBar().showMessage("已取消圆柱凸台高度调整")
return
self._show_cylinder_boss_height_preview(face_id, target_height)
self.clear_edit_preview(render=False)
def action():
return self.model.resize_cylindrical_boss_height(face_id, target_height)
@@ -2451,6 +2991,11 @@ class WindowActionMixin:
},
target_kind="face",
target_id=face_id,
isolation=self._isolation_for_plan(
plan,
"resize_cylindrical_boss_height",
[face_id, target_height],
),
)
def resize_cylinder_height(self) -> None:
@@ -2527,6 +3072,7 @@ class WindowActionMixin:
},
target_kind="face",
target_id=face_id,
isolation=self._isolation_for_plan(plan, "resize_cylindrical_height", [face_id, target_height]),
)
def resize_cylindrical_height_owning_scale(self) -> None:
@@ -2616,6 +3162,11 @@ class WindowActionMixin:
},
target_kind="face",
target_id=face_id,
isolation=self._isolation_for_plan(
plan,
"resize_cylindrical_height_owning_scale",
[face_id, target_height],
),
)
def suppress_hole(self) -> None:
@@ -4659,7 +5210,9 @@ class WindowActionMixin:
return
face_id = self.selected_face_id
plan = self.model.face_area_local_resize_plan(face_id, target_area)
plan = self._quick_blocked_face_area_local_plan(face_id, target_area)
if plan is None:
plan = self.model.face_area_local_resize_plan(face_id, target_area)
lines = [
f"Face: {face_id}",
f"当前面积: {_format_value(plan.get('current_area'))}",
@@ -4750,7 +5303,9 @@ class WindowActionMixin:
return
face_id = self.selected_face_id
plan = self.model.face_size_local_resize_plan(face_id, target_size, axis_key)
plan = self._quick_blocked_face_size_local_plan(face_id, target_size, axis_key)
if plan is None:
plan = self.model.face_size_local_resize_plan(face_id, target_size, axis_key)
lines = [
f"Face: {face_id}",
f"修改对象: {axis_label}(当前面)",
@@ -5010,7 +5565,9 @@ class WindowActionMixin:
return
face_id = self.selected_face_id
plan = self.model.face_plane_offset_local_plan(face_id, distance)
plan = self._quick_blocked_face_plane_offset_local_plan(face_id, distance)
if plan is None:
plan = self.model.face_plane_offset_local_plan(face_id, distance)
lines = [
f"Face: {face_id}",
f"当前面偏移: {_format_value(plan.get('current_plane_position'))}",
@@ -5177,7 +5734,9 @@ class WindowActionMixin:
current_center[1] + vector[1],
current_center[2] + vector[2],
)
plan = self.model.face_center_local_move_plan(face_id, target_center)
plan = self._quick_blocked_face_center_local_plan(face_id, target_center)
if plan is None:
plan = self.model.face_center_local_move_plan(face_id, target_center)
lines = [
f"Face: {face_id}",
f"当前Face中心: {_format_value(plan.get('current_face_center'))}",
@@ -6450,7 +7009,10 @@ class WindowActionMixin:
raise RuntimeError(
f"编辑失败,且回滚到操作前状态也失败:{rollback_exc}\n原始错误:{exc}"
) from exc
raise RuntimeError(f"编辑失败,模型已恢复到操作前状态:{exc}") from exc
raise RuntimeError(
f"编辑失败,模型已恢复到操作前状态:{exc}"
f"{self._edit_failure_diagnostics(context)}"
) from exc
model_polydata = None
edge_polydata = None
try:
@@ -6522,22 +7084,38 @@ class WindowActionMixin:
)
command = self._isolated_edit_command(request_path)
self.isolated_edit_cancel_requested = False
process: subprocess.Popen[str] | None = None
try:
completed = subprocess.run(
process = subprocess.Popen(
command,
cwd=project_root,
capture_output=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
timeout=timeout_seconds,
check=False,
)
self.active_isolated_edit_process = process
stdout, stderr = process.communicate(timeout=timeout_seconds)
completed = subprocess.CompletedProcess(command, process.returncode, stdout, stderr)
except subprocess.TimeoutExpired as exc:
self._terminate_isolated_edit_process(process)
try:
stdout, stderr = process.communicate(timeout=5.0) if process is not None else ("", "")
except Exception:
stdout, stderr = "", ""
raise RuntimeError(
f"隔离子进程执行超时,已终止危险计算;主程序和原模型保持不变。"
f" 超时时间: {timeout_seconds:g}s"
f"{self._edit_failure_diagnostics(context)}"
) from exc
finally:
if getattr(self, "active_isolated_edit_process", None) is process:
self.active_isolated_edit_process = None
if getattr(self, "isolated_edit_cancel_requested", False):
raise RuntimeError("隔离子进程已取消;主程序和原模型保持不变。")
response_path = request_path.with_suffix(".response.json")
response: dict[str, object] = {}
@@ -6551,6 +7129,7 @@ class WindowActionMixin:
raise RuntimeError(
"隔离子进程执行失败;主程序没有崩溃,原模型保持不变。"
f" 子进程返回码: {completed.returncode}. 错误: {error}"
f"{self._edit_failure_diagnostics(context)}"
)
if not output_path.exists():
raise RuntimeError("隔离子进程报告成功,但没有生成结果 STEP;原模型保持不变。")
@@ -6576,14 +7155,13 @@ class WindowActionMixin:
after_quality,
after_model=new_model,
)
self.model = new_model
after_geometry: dict[str, object] = {}
model_polydata = None
edge_polydata = None
try:
deflection = float(context.get("edit_result_deflection", 1.6))
model_polydata = self.model.build_face_polydata(deflection=deflection)
edge_polydata = self.model.build_edge_polydata(
model_polydata = new_model.build_face_polydata(deflection=deflection)
edge_polydata = new_model.build_edge_polydata(
deflection=deflection,
show_same_domain_internal_edges=bool(context.get("show_same_domain_internal_edges", False)),
)
@@ -6600,6 +7178,7 @@ class WindowActionMixin:
"after_snapshot": after_snapshot,
"after_stats": after_stats,
"after_part_stats": after_part_stats,
"after_model": new_model,
"quality_warnings": quality_warnings,
"after_geometry": after_geometry,
"model_polydata": model_polydata,
@@ -6611,6 +7190,35 @@ class WindowActionMixin:
return [sys.executable, "--isolated-edit-worker", str(request_path)]
return [sys.executable, "-m", "step_editor.isolated_edit_worker", str(request_path)]
def _terminate_isolated_edit_process(self, process: subprocess.Popen | None = None) -> bool:
target = process or getattr(self, "active_isolated_edit_process", None)
if target is None:
return False
try:
if target.poll() is not None:
return False
except Exception:
return False
self.isolated_edit_cancel_requested = True
try:
target.terminate()
return True
except Exception:
try:
target.kill()
return True
except Exception:
return False
def _cancel_active_edit_for_close(self) -> bool:
if not (self.operation_in_progress or (self.edit_thread is not None and self.edit_thread.isRunning())):
return False
if not self._terminate_isolated_edit_process():
return False
self.close_after_edit_cancel = True
self.statusBar().showMessage("正在取消后台几何计算,子进程已请求终止...")
return True
def _preserve_isolated_face_logical_id(
self,
model: StepModel,
@@ -7285,6 +7893,9 @@ class WindowActionMixin:
QMessageBox.critical(self, "操作失败", "后台编辑返回了无法识别的结果。")
self.statusBar().showMessage("编辑结果无法识别")
return
after_model = result.get("after_model")
if after_model is not None:
self.model = after_model
message = str(result["message"])
try:
record = self._make_operation_record(
@@ -7347,11 +7958,20 @@ class WindowActionMixin:
if hasattr(self, "_is_ui_thread") and not self._is_ui_thread():
self._invoke_on_ui_thread(lambda message=message: self._fail_edit_action(message))
return
close_after_cancel = bool(getattr(self, "close_after_edit_cancel", False))
if close_after_cancel:
self.close_after_edit_cancel = False
self._end_edit_task(clear_preview=True)
QMessageBox.critical(self, "操作失败", message)
if not close_after_cancel:
QMessageBox.critical(self, "操作失败", message)
self._clear_editable_candidates()
self._clear_cylinder_candidates()
self.statusBar().showMessage("操作失败,模型已保持在编辑前状态")
self.statusBar().showMessage("后台编辑已取消,模型已保持在编辑前状态" if close_after_cancel else "操作失败,模型已保持在编辑前状态")
if close_after_cancel:
if self.edit_thread is not None and self.edit_thread.isRunning():
self.edit_thread.quit()
self.edit_thread.wait(1500)
self.close()
def _restore_failed_edit_snapshot(self, snapshot: object) -> str:
if self.model is None or not isinstance(snapshot, dict):