feat: 推进SCDM-first后端接入和大模型编辑优化
This commit is contained in:
+242
-17
@@ -106,6 +106,7 @@ def _edit_timing_summary(timings: object, *, limit: int = 5) -> str:
|
||||
"validate": "结果校验",
|
||||
"display_faces": "面显示",
|
||||
"display_edges": "边线",
|
||||
"result_face_mapping": "结果Face定位",
|
||||
"finish_ui": "界面刷新",
|
||||
"total": "总计",
|
||||
}
|
||||
@@ -507,7 +508,14 @@ class WindowActionMixin:
|
||||
if plan["status"] == "blocked":
|
||||
self._show_blocked_plan_message(operation_name, plan, "拉伸/切除平面已阻止")
|
||||
return
|
||||
if plan["risk"] != "low":
|
||||
if keep_relations:
|
||||
operation_key = "push_pull_face_keep_relations"
|
||||
isolation = self._isolation_for_plan(plan, operation_key, [face_id, distance])
|
||||
else:
|
||||
operation_key = "push_pull_face"
|
||||
isolation = self._isolation_for_plan(plan, operation_key, [face_id, distance])
|
||||
|
||||
if plan["risk"] != "low" and not self._can_skip_edit_confirmation(plan, isolation):
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||||
isolation_line = (
|
||||
@@ -538,10 +546,6 @@ class WindowActionMixin:
|
||||
if result != QMessageBox.StandardButton.Yes:
|
||||
self.statusBar().showMessage("已取消拉伸/切除平面")
|
||||
return
|
||||
if keep_relations:
|
||||
isolation = self._isolation_for_plan(plan, "push_pull_face_keep_relations", [face_id, distance])
|
||||
else:
|
||||
isolation = self._isolation_for_plan(plan, "push_pull_face", [face_id, distance])
|
||||
if isolation is None:
|
||||
self._show_push_pull_preview(face_id, distance, plan=plan)
|
||||
else:
|
||||
@@ -629,6 +633,7 @@ class WindowActionMixin:
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=isolation,
|
||||
operation_key=operation_key,
|
||||
)
|
||||
|
||||
def _quick_push_pull_plan(self, face_id: int, distance: float) -> dict[str, object]:
|
||||
@@ -840,13 +845,31 @@ class WindowActionMixin:
|
||||
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)
|
||||
inner_wires = int(
|
||||
quick_plan.get("inner_boundary_wires")
|
||||
or quick_plan.get("selected_inner_boundary_wires")
|
||||
or 0
|
||||
)
|
||||
boundary_wires = int(
|
||||
quick_plan.get("boundary_wires")
|
||||
or quick_plan.get("selected_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.
|
||||
boundary_edges = int(
|
||||
quick_plan.get("first_level_boundary_edge_count")
|
||||
or quick_plan.get("selected_boundary_edge_count")
|
||||
or 0
|
||||
)
|
||||
if boundary_wires and boundary_wires <= 16 and inner_wires <= 12:
|
||||
return False
|
||||
if boundary_edges and boundary_edges <= 120 and inner_wires <= 12:
|
||||
return False
|
||||
|
||||
# Very large STEP + extremely fragmented holed caps can still spend
|
||||
# noticeable time scanning topology before the actual edit starts.
|
||||
return abs(float(distance)) > 1e-9
|
||||
|
||||
def _deferred_push_pull_model_plan(
|
||||
@@ -1727,13 +1750,66 @@ class WindowActionMixin:
|
||||
return None
|
||||
if risk not in {"low", "medium", "high"}:
|
||||
return None
|
||||
prefer_smooth_process = self._prefer_isolated_process_for_large_interactive_edit(plan, operation)
|
||||
if not prefer_smooth_process and self._can_run_inprocess_background_edit(plan, operation):
|
||||
return None
|
||||
return {
|
||||
"operation": operation,
|
||||
"args": args,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
"reason": f"{risk}-risk-isolated-occ-edit",
|
||||
"reason": "large-model-smooth-ui-isolated-occ-edit" if prefer_smooth_process else f"{risk}-risk-isolated-occ-edit",
|
||||
}
|
||||
|
||||
def _can_run_inprocess_background_edit(self, plan: dict[str, object], operation: str) -> bool:
|
||||
if operation != "push_pull_face":
|
||||
return False
|
||||
if str(plan.get("planar_cap_extension_method") or "") == "boundary-shell-rebuild":
|
||||
return True
|
||||
if str(plan.get("cylindrical_cap_extension_method") or "") == "local-shell-rebuild":
|
||||
return True
|
||||
if bool(plan.get("ui_deferred_model_plan")) and int(plan.get("selected_inner_boundary_wires", 0) or 0) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _prefer_isolated_process_for_large_interactive_edit(self, plan: dict[str, object], operation: str) -> bool:
|
||||
if operation not in {"push_pull_face", "push_pull_face_keep_relations"}:
|
||||
return False
|
||||
method = str(plan.get("planar_cap_extension_method") or plan.get("cylindrical_cap_extension_method") or "")
|
||||
if method not in {"boundary-shell-rebuild", "local-shell-rebuild"}:
|
||||
return False
|
||||
if method == "local-shell-rebuild":
|
||||
return True
|
||||
if bool(getattr(self, "_large_model_interaction_mode", lambda: False)()):
|
||||
return True
|
||||
boundary_edges = _int_or_none(plan.get("first_level_boundary_edge_count")) or _int_or_none(
|
||||
plan.get("planar_cap_boundary_edge_count")
|
||||
) or 0
|
||||
adjacent_faces = _int_or_none(plan.get("first_level_adjacent_face_count")) or _int_or_none(
|
||||
plan.get("planar_cap_adjacent_face_count")
|
||||
) or 0
|
||||
inner_wires = _int_or_none(plan.get("selected_inner_boundary_wires")) or _int_or_none(
|
||||
plan.get("planar_cap_inner_boundary_wires")
|
||||
) or 0
|
||||
return boundary_edges >= 32 or adjacent_faces >= 32 or inner_wires >= 2
|
||||
|
||||
def _skip_before_quality_check_for_large_edit(
|
||||
self,
|
||||
operation_name: str,
|
||||
operation_key: str | None = None,
|
||||
) -> bool:
|
||||
if operation_key not in {"push_pull_face", "push_pull_face_keep_relations"} and "拉伸/切除" not in str(
|
||||
operation_name or ""
|
||||
):
|
||||
return False
|
||||
return bool(getattr(self, "_large_model_interaction_mode", lambda: False)())
|
||||
|
||||
def _can_skip_edit_confirmation(self, plan: dict[str, object], isolation: dict[str, object] | None) -> bool:
|
||||
if str(plan.get("status") or "") == "blocked":
|
||||
return False
|
||||
if not isinstance(isolation, dict):
|
||||
return False
|
||||
return isolation.get("reason") == "large-model-smooth-ui-isolated-occ-edit"
|
||||
|
||||
def _edit_failure_diagnostics(self, context: dict[str, object]) -> str:
|
||||
parameters = context.get("parameters")
|
||||
if not isinstance(parameters, dict):
|
||||
@@ -7683,6 +7759,8 @@ class WindowActionMixin:
|
||||
|
||||
@Slot(object)
|
||||
def _finish_scan_task_result(self, result: object) -> None:
|
||||
if hasattr(self, "_reroute_to_ui_thread") and self._reroute_to_ui_thread(lambda result=result: self._finish_scan_task_result(result)):
|
||||
return
|
||||
scan_kind = self.pending_scan_kind
|
||||
context = dict(self.pending_scan_context or {})
|
||||
if scan_kind == "editable":
|
||||
@@ -7701,6 +7779,8 @@ class WindowActionMixin:
|
||||
|
||||
@Slot(str)
|
||||
def _fail_scan_task_result(self, message: str) -> None:
|
||||
if hasattr(self, "_reroute_to_ui_thread") and self._reroute_to_ui_thread(lambda message=message: self._fail_scan_task_result(message)):
|
||||
return
|
||||
scan_kind = self.pending_scan_kind
|
||||
if scan_kind == "editable":
|
||||
self._fail_editable_scan(message)
|
||||
@@ -7978,6 +8058,7 @@ class WindowActionMixin:
|
||||
target_kind: str | None = None,
|
||||
target_id: int | None = None,
|
||||
isolation: dict[str, object] | None = None,
|
||||
operation_key: str | None = None,
|
||||
) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
@@ -7985,11 +8066,15 @@ class WindowActionMixin:
|
||||
self.statusBar().showMessage("后台编辑正在计算,请等待当前操作完成。")
|
||||
return
|
||||
target_logical_id = self._edit_target_logical_id(target_kind, target_id)
|
||||
result_deflection = float(getattr(self, "edit_result_deflection", 1.6))
|
||||
result_deflection = _large_model_display_deflection_for_model(
|
||||
self.model,
|
||||
float(getattr(self, "edit_result_deflection", 1.6)),
|
||||
)
|
||||
if operation_name == "拉伸/切除平面":
|
||||
result_deflection = max(result_deflection, 0.35)
|
||||
context = {
|
||||
"operation_name": operation_name,
|
||||
"operation_key": operation_key or "",
|
||||
"target": target,
|
||||
"parameters": parameters,
|
||||
"target_kind": target_kind,
|
||||
@@ -8000,6 +8085,10 @@ class WindowActionMixin:
|
||||
"edit_result_deflection": result_deflection,
|
||||
"defer_edge_polydata": True,
|
||||
"isolation": dict(isolation or {}),
|
||||
"skip_before_quality_check": self._skip_before_quality_check_for_large_edit(
|
||||
operation_name,
|
||||
operation_key,
|
||||
),
|
||||
}
|
||||
blocker = self._edit_preflight_blocker(context)
|
||||
if blocker is not None:
|
||||
@@ -8046,7 +8135,11 @@ class WindowActionMixin:
|
||||
target_part_id = self._edit_context_part_id(context)
|
||||
before_stats = self.model.stats()
|
||||
before_part_stats = self._part_stats_or_none(target_part_id)
|
||||
before_quality = self._edit_quality_info_or_none(self.model, context, target_part_id)
|
||||
before_quality = (
|
||||
None
|
||||
if bool(context.get("skip_before_quality_check"))
|
||||
else self._edit_quality_info_or_none(self.model, context, target_part_id)
|
||||
)
|
||||
before_geometry = {}
|
||||
timings["snapshot"] = time.perf_counter() - started
|
||||
isolation = context.get("isolation")
|
||||
@@ -8262,7 +8355,9 @@ class WindowActionMixin:
|
||||
except Exception:
|
||||
pass
|
||||
child_message = str(response.get("message") or "隔离子进程编辑完成。")
|
||||
started = time.perf_counter()
|
||||
self._preserve_isolated_face_logical_id(new_model, context, child_message)
|
||||
timings["result_face_mapping"] = time.perf_counter() - started
|
||||
started = time.perf_counter()
|
||||
after_snapshot = new_model.snapshot()
|
||||
after_stats = new_model.stats()
|
||||
@@ -8305,7 +8400,7 @@ class WindowActionMixin:
|
||||
timings["total"] = time.perf_counter() - total_started
|
||||
|
||||
return {
|
||||
"message": f"{child_message} 已通过隔离子进程完成;如果 OCC 崩溃,主程序不会被带崩。",
|
||||
"message": f"{child_message} 已通过独立后台几何进程完成,主界面会保持可响应。",
|
||||
"snapshot": snapshot,
|
||||
"before_stats": before_stats,
|
||||
"before_part_stats": before_part_stats,
|
||||
@@ -8375,9 +8470,6 @@ class WindowActionMixin:
|
||||
logical_id = int(target_logical_id)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
candidate_ids: list[int] = []
|
||||
if 0 <= face_id < len(model.faces):
|
||||
candidate_ids.append(face_id)
|
||||
parameters = context.get("parameters")
|
||||
parameters = parameters if isinstance(parameters, dict) else {}
|
||||
target_position = _float_or_none(parameters.get("target_plane_position"))
|
||||
@@ -8385,6 +8477,29 @@ class WindowActionMixin:
|
||||
_unit_triple_or_none(parameters.get("plane_direction"))
|
||||
or _unit_triple_or_none(parameters.get("outward_direction"))
|
||||
)
|
||||
if 0 <= face_id < len(model.faces):
|
||||
if target_position is not None and plane_direction is not None:
|
||||
if self._face_target_plane_position_matches(
|
||||
model,
|
||||
[face_id],
|
||||
target_position,
|
||||
plane_direction,
|
||||
_float_or_none(parameters.get("bbox_diagonal")),
|
||||
):
|
||||
try:
|
||||
model.assign_logical_face_region_exclusive(logical_id, [face_id])
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
elif self._assign_isolated_logical_face_candidate(model, logical_id, face_id, context):
|
||||
return
|
||||
|
||||
isolation = context.get("isolation")
|
||||
if isinstance(isolation, dict) and isolation.get("reason") == "large-model-smooth-ui-isolated-occ-edit":
|
||||
return
|
||||
candidate_ids: list[int] = []
|
||||
if 0 <= face_id < len(model.faces):
|
||||
candidate_ids.append(face_id)
|
||||
if target_position is not None and plane_direction is not None:
|
||||
part_id = self._edit_integrity_int_or_none(parameters.get("part_id"))
|
||||
solid_id = self._edit_integrity_int_or_none(parameters.get("solid_id"))
|
||||
@@ -8422,6 +8537,26 @@ class WindowActionMixin:
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def _assign_isolated_logical_face_candidate(
|
||||
self,
|
||||
model: StepModel,
|
||||
logical_id: int,
|
||||
face_id: int,
|
||||
context: dict[str, object],
|
||||
) -> bool:
|
||||
try:
|
||||
if bool(getattr(self, "_large_model_interaction_mode", lambda: False)()) or (
|
||||
isinstance(context.get("isolation"), dict)
|
||||
and context["isolation"].get("reason") == "large-model-smooth-ui-isolated-occ-edit"
|
||||
):
|
||||
face_ids = [int(face_id)]
|
||||
else:
|
||||
face_ids = model.face_region_ids(int(face_id)) or [int(face_id)]
|
||||
model.assign_logical_face_region_exclusive(int(logical_id), face_ids)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _edit_context_part_id(self, context: dict[str, object]) -> int | None:
|
||||
if self.model is None:
|
||||
return None
|
||||
@@ -9169,10 +9304,16 @@ class WindowActionMixin:
|
||||
pick_position=context["pick_position"],
|
||||
before_snapshot=result["snapshot"],
|
||||
after_snapshot=result["after_snapshot"],
|
||||
isolation=dict(context.get("isolation") or {}),
|
||||
)
|
||||
model_polydata = result.get("model_polydata")
|
||||
edge_polydata = result.get("edge_polydata")
|
||||
edge_deferred = bool(result.get("edge_polydata_deferred"))
|
||||
large_model = len(getattr(self.model, "faces", ()) or ()) > 1000 or len(getattr(self.model, "edges", ()) or ()) > 2500
|
||||
if edge_deferred and large_model:
|
||||
self.large_model_edge_overlay_skipped = True
|
||||
elif not edge_deferred:
|
||||
self.large_model_edge_overlay_skipped = False
|
||||
if model_polydata is None or (edge_polydata is None and not edge_deferred):
|
||||
deflection = float(context.get("edit_result_deflection", 1.6))
|
||||
model_polydata = self.model.build_face_polydata(deflection=deflection)
|
||||
@@ -9213,7 +9354,8 @@ class WindowActionMixin:
|
||||
self._refresh_history_list()
|
||||
self._end_edit_task(clear_preview=False)
|
||||
timing_text = _edit_timing_summary(result.get("timings"))
|
||||
if bool(result.get("edge_polydata_deferred")):
|
||||
edge_deferred = bool(result.get("edge_polydata_deferred"))
|
||||
if edge_deferred and not bool(getattr(self, "large_model_edge_overlay_skipped", False)):
|
||||
QTimer.singleShot(80, self._rebuild_deferred_edge_display)
|
||||
if result.get("quality_warnings"):
|
||||
self.statusBar().showMessage("编辑完成,但有质量警告,请查看操作历史详情")
|
||||
@@ -9221,6 +9363,8 @@ class WindowActionMixin:
|
||||
selection_note = ";已保持当前选择" if self.selected_kind is not None else ""
|
||||
timing_note = f";耗时 {timing_text}" if timing_text else ""
|
||||
edge_note = ";边线稍后补充" if bool(result.get("edge_polydata_deferred")) else ""
|
||||
if edge_deferred and bool(getattr(self, "large_model_edge_overlay_skipped", False)):
|
||||
edge_note = ";边线按需生成"
|
||||
self.statusBar().showMessage(f"{message}{selection_note}{timing_note}{edge_note}")
|
||||
if self.selected_kind is None:
|
||||
timing_detail = f"\n\n性能耗时:{timing_text}" if timing_text else ""
|
||||
@@ -9293,6 +9437,84 @@ class WindowActionMixin:
|
||||
self.edit_thread = None
|
||||
self.edit_worker = None
|
||||
|
||||
def _operation_parameters_with_recognition_sources(
|
||||
self,
|
||||
parameters: dict[str, object],
|
||||
target_kind: str | None,
|
||||
target_id: int | None,
|
||||
) -> dict[str, object]:
|
||||
result = dict(parameters or {})
|
||||
if result.get("recognition_source") not in {None, ""}:
|
||||
return result
|
||||
evidence = [result, getattr(self, "current_info_values", {})]
|
||||
if (
|
||||
target_kind in {"face", "feature"}
|
||||
and target_id is not None
|
||||
and getattr(self, "model", None) is not None
|
||||
):
|
||||
try:
|
||||
evidence.append(self.model.quick_face_info(int(target_id)))
|
||||
except Exception:
|
||||
pass
|
||||
result["recognition_source"] = (
|
||||
"Analysis Situs + internal StepModel"
|
||||
if any(self._operation_has_analysis_situs_evidence(item) for item in evidence)
|
||||
else "internal StepModel"
|
||||
)
|
||||
return result
|
||||
|
||||
def _operation_backend_log_lines(
|
||||
self,
|
||||
parameters: dict[str, object],
|
||||
result_message: str,
|
||||
*,
|
||||
isolation: dict[str, object] | None = None,
|
||||
) -> list[str]:
|
||||
execution = "isolated OCCT subprocess" if isinstance(isolation, dict) and isolation else "Qt background worker"
|
||||
recognition = str(parameters.get("recognition_source") or "").strip()
|
||||
if not recognition:
|
||||
recognition = (
|
||||
"Analysis Situs + internal StepModel"
|
||||
if self._operation_has_analysis_situs_evidence(parameters)
|
||||
or self._operation_has_analysis_situs_evidence(result_message)
|
||||
else "internal StepModel"
|
||||
)
|
||||
return [
|
||||
"backend: OCCT",
|
||||
f"execution: {execution}",
|
||||
f"recognition: {recognition}",
|
||||
]
|
||||
|
||||
def _operation_has_analysis_situs_evidence(self, value: object) -> bool:
|
||||
if self._operation_value_is_empty(value):
|
||||
return False
|
||||
if isinstance(value, str):
|
||||
lowered = value.lower()
|
||||
return "analysis situs" in lowered or "analysis-situs" in lowered
|
||||
if isinstance(value, dict):
|
||||
for key, item in value.items():
|
||||
key_text = str(key).lower()
|
||||
if (
|
||||
key_text.startswith("asitus_")
|
||||
or key_text.startswith("analysis_situs_")
|
||||
or key_text.startswith("external_recognition_")
|
||||
) and not self._operation_value_is_empty(item):
|
||||
return True
|
||||
if self._operation_has_analysis_situs_evidence(item):
|
||||
return True
|
||||
return False
|
||||
if isinstance(value, (tuple, list, set)):
|
||||
return any(self._operation_has_analysis_situs_evidence(item) for item in value)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _operation_value_is_empty(value: object) -> bool:
|
||||
if value is None or value == "":
|
||||
return True
|
||||
if isinstance(value, (tuple, list, set, dict)) and not value:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _make_operation_record(
|
||||
self,
|
||||
operation_name: str,
|
||||
@@ -9312,7 +9534,9 @@ class WindowActionMixin:
|
||||
pick_position: tuple[float, float, float] | None = None,
|
||||
before_snapshot: dict[int, object] | None = None,
|
||||
after_snapshot: dict[int, object] | None = None,
|
||||
isolation: dict[str, object] | None = None,
|
||||
) -> OperationRecord:
|
||||
parameters = self._operation_parameters_with_recognition_sources(parameters, target_kind, target_id)
|
||||
target_summary = target
|
||||
if target_kind in {"face", "feature"} and target_logical_id is not None:
|
||||
target_summary = f"{target_kind} logical {target_logical_id}"
|
||||
@@ -9377,6 +9601,7 @@ class WindowActionMixin:
|
||||
f"target: {target}",
|
||||
f"target_kind: {target_kind or ''}",
|
||||
f"target_id: {target_id if target_id is not None else ''}",
|
||||
*self._operation_backend_log_lines(parameters, result_message, isolation=isolation),
|
||||
"parameters:",
|
||||
]
|
||||
if target_logical_id is not None:
|
||||
|
||||
Reference in New Issue
Block a user