feat: 增加Face偏移保持一级关系策略

This commit is contained in:
2026-08-10 15:26:35 +08:00
parent 83c44265ba
commit 415e8a0f27
9 changed files with 357 additions and 16 deletions
+2
View File
@@ -24,6 +24,8 @@ def _point3(value: object, operation: str) -> tuple[float, float, float]:
def _execute(model: StepModel, operation: str, args: list[object]) -> str:
if operation == "push_pull_face":
return model.push_pull_face(int(args[0]), float(args[1]))
if operation == "push_pull_face_keep_relations":
return model.push_pull_face_keep_relations(int(args[0]), float(args[1]))
if operation == "move_face_plane_offset_local":
return model.move_face_plane_offset_local(int(args[0]), float(args[1]))
if operation == "resize_face_area_local":
+180 -2
View File
@@ -258,6 +258,116 @@ class OperationMixin:
blockers.append(f"{operation_label}缺少直接相邻 Face 角色,不能确定一级联动范围。")
return blockers
def _face_push_pull_planar_relation_constraint_fields(
self,
face_id: int,
plan: dict[str, object],
) -> dict[str, object]:
relation_status = str(plan.get("first_level_planar_relation_status") or "")
relation_scope = str(plan.get("first_level_planar_relation_scope") or "")
relation_count = int(plan.get("first_level_planar_relation_count", 0) or 0)
parallel_count = int(plan.get("first_level_planar_relation_parallel_count", 0) or 0)
perpendicular_count = int(plan.get("first_level_planar_relation_perpendicular_count", 0) or 0)
angled_count = int(plan.get("first_level_planar_relation_angled_count", 0) or 0)
adjacent_count = int(plan.get("first_level_adjacent_face_count", 0) or 0)
subject_count = int(plan.get("same_domain_face_count", 0) or 0)
non_planar_face_ids = _int_values(plan.get("first_level_planar_relation_non_planar_face_ids"))
observed_modes = tuple(str(item) for item in plan.get("first_level_planar_relation_observed_modes", ()) or ())
rows = tuple(row for row in plan.get("first_level_planar_relation_rows", ()) or () if isinstance(row, dict))
blockers: list[str] = []
warnings: list[str] = []
if relation_status != "ready" or relation_scope != "face":
blockers.append("当前 Face 的一级平面关系还不可验证,不能使用“保持关系”建模意图。")
if subject_count <= 0:
blockers.append("没有识别到当前 Face 的同域主体区域,不能判断哪些面应一起参与保持关系。")
if adjacent_count <= 0:
blockers.append("没有识别到共享边一级相邻 Face,保持关系没有可验证对象。")
if relation_count <= 0:
blockers.append("当前一级邻域没有识别到平行或垂直平面关系。")
if non_planar_face_ids:
blockers.append(
"一级相邻 Face 中包含非平面 Face "
f"{tuple(non_planar_face_ids)};当前“保持关系”只支持平面邻域。"
)
if angled_count > 0:
blockers.append("一级相邻平面中存在斜交关系;当前“保持关系”只支持平行/垂直关系。")
if parallel_count + perpendicular_count <= 0:
blockers.append("没有可保持的一级平行/垂直关系。")
expected_planar_adjacent = max(adjacent_count - len(non_planar_face_ids), 0)
if relation_count < max(expected_planar_adjacent, 1):
blockers.append(
"一级平面关系数量少于相邻平面数量,说明还有相邻面关系无法判断;"
"当前版本会先阻止,避免只移动局部面后破坏周边结构。"
)
if not blockers:
warnings.append("将执行后反查一级相邻平面关系;如果关系丢失,模型会回滚。")
if blockers:
summary = (
"Face保持关系:当前不能稳定保持一级平面关系;"
f"相邻 Face {adjacent_count},平行 {parallel_count},垂直 {perpendicular_count}"
f"斜交 {angled_count},非平面 {len(non_planar_face_ids)}"
)
else:
summary = (
"Face保持关系:可按拉伸/切除执行并反查一级平面关系;"
f"平行 {parallel_count},垂直 {perpendicular_count}"
)
return {
"face_push_pull_planar_relation_constraint_requested": True,
"face_push_pull_planar_relation_constraint_label": "保持一级平面关系",
"face_push_pull_planar_constraint_status": "blocked" if blockers else "ready",
"face_push_pull_planar_constraint_risk": "blocked" if blockers else ("medium" if warnings else "low"),
"face_push_pull_planar_constraint_summary": summary,
"face_push_pull_planar_constraint_relation_count": relation_count,
"face_push_pull_planar_constraint_parallel_count": parallel_count,
"face_push_pull_planar_constraint_perpendicular_count": perpendicular_count,
"face_push_pull_planar_constraint_angled_count": angled_count,
"face_push_pull_planar_constraint_adjacent_face_count": adjacent_count,
"face_push_pull_planar_constraint_subject_face_count": subject_count,
"face_push_pull_planar_constraint_non_planar_face_ids": tuple(non_planar_face_ids),
"face_push_pull_planar_constraint_observed_modes": observed_modes,
"face_push_pull_planar_constraint_rows": rows,
"face_push_pull_planar_constraint_blockers": "".join(blockers),
"face_push_pull_planar_constraint_warnings": "".join(warnings),
"face_push_pull_planar_constraint_note": (
"Face保持关系当前只作为一级平面关系守门:执行前要求直接相邻 Face 的平行/垂直关系可验证,"
"执行后在结果 Face 上反查;二级/三级传播仍不自动处理。"
),
}
def push_pull_keep_relations_plan(self, face_id: int, distance: float) -> dict[str, object]:
plan = self.push_pull_plan(face_id, distance)
constraint_fields = self._face_push_pull_planar_relation_constraint_fields(face_id, plan)
plan.update(constraint_fields)
plan["resize_strategy"] = "push-pull-planar-face-keep-relations"
plan["edit_strategy_label"] = "保持一级平面关系的拉伸/切除"
plan["edit_semantics"] = (
"沿当前 Face 法向拉伸/切除,同时要求当前 Face 与共享边一级相邻平面的平行/垂直关系"
"在执行前可验证,并在执行后反查。"
)
if plan["status"] != "blocked" and constraint_fields["face_push_pull_planar_constraint_status"] == "blocked":
blockers = str(constraint_fields.get("face_push_pull_planar_constraint_blockers") or "")
plan["status"] = "blocked"
plan["risk"] = "blocked"
plan["message"] = blockers or str(constraint_fields["face_push_pull_planar_constraint_summary"])
existing_blockers = str(plan.get("blockers") or "").strip()
plan["blockers"] = "".join(item for item in (existing_blockers, blockers) if item)
elif plan["status"] != "blocked":
warnings = [
item for item in str(plan.get("warnings") or "").split("") if item
]
constraint_warning = str(constraint_fields.get("face_push_pull_planar_constraint_warnings") or "").strip()
if constraint_warning:
warnings.append(constraint_warning)
plan["warnings"] = "".join(warnings)
if plan["risk"] == "low":
plan["message"] = str(constraint_fields["face_push_pull_planar_constraint_summary"])
return plan
def _multistep_prismatic_plan_blocker(self, face_id: int, operation_label: str) -> str:
try:
feature = self.feature_info(face_id)
@@ -8189,6 +8299,57 @@ class OperationMixin:
part.shape = shape
self.refresh_topology()
def _face_push_pull_planar_relation_result_summary_or_raise(
self,
plan: dict[str, object],
matched_face_id: int,
facts: dict[str, object],
) -> str:
if plan.get("face_push_pull_planar_relation_constraint_requested") is not True:
return ""
if str(plan.get("face_push_pull_planar_constraint_status") or "") != "ready":
raise RuntimeError("Face planar relation check was requested but the original constraint plan is not ready.")
actual_status = str(facts.get("first_level_planar_relation_status") or "")
actual_relation_count = int(facts.get("first_level_planar_relation_count", 0) or 0)
actual_parallel = int(facts.get("first_level_planar_relation_parallel_count", 0) or 0)
actual_perpendicular = int(facts.get("first_level_planar_relation_perpendicular_count", 0) or 0)
actual_angled = int(facts.get("first_level_planar_relation_angled_count", 0) or 0)
actual_adjacent = int(facts.get("first_level_fact_adjacent_face_count", 0) or 0)
actual_non_planar = _int_values(facts.get("first_level_planar_relation_non_planar_face_ids"))
expected_relation_count = int(plan.get("face_push_pull_planar_constraint_relation_count", 0) or 0)
expected_parallel = int(plan.get("face_push_pull_planar_constraint_parallel_count", 0) or 0)
expected_perpendicular = int(plan.get("face_push_pull_planar_constraint_perpendicular_count", 0) or 0)
expected_adjacent = int(plan.get("face_push_pull_planar_constraint_adjacent_face_count", 0) or 0)
failures: list[str] = []
if actual_status != "ready":
failures.append(f"结果 Face {matched_face_id} 的一级平面关系不是 ready")
if actual_adjacent < expected_adjacent:
failures.append(f"一级相邻 Face 数减少 {actual_adjacent}/{expected_adjacent}")
if actual_relation_count < expected_relation_count:
failures.append(f"一级平面关系数减少 {actual_relation_count}/{expected_relation_count}")
if actual_parallel < expected_parallel:
failures.append(f"平行关系数减少 {actual_parallel}/{expected_parallel}")
if actual_perpendicular < expected_perpendicular:
failures.append(f"垂直关系数减少 {actual_perpendicular}/{expected_perpendicular}")
if actual_angled > 0:
failures.append(f"结果一级邻域出现 {actual_angled} 条斜交关系")
if actual_non_planar:
failures.append(f"结果一级邻域出现非平面 Face {tuple(actual_non_planar)}")
if failures:
raise RuntimeError(
"Face保持关系反查失败,模型已恢复到修改前状态。"
+ "".join(failures)
)
return (
" Planar relation check: ok, "
f"face={matched_face_id}, relations={actual_relation_count}, "
f"parallel={actual_parallel}, perpendicular={actual_perpendicular}, angled={actual_angled}."
)
def _checked_face_edit_result_summary(self, plan: dict[str, object]) -> str:
part_id = _int_or_none(plan.get("part_id"))
part = self.part_by_id(part_id) if part_id is not None else None
@@ -8369,12 +8530,18 @@ class OperationMixin:
f"required_after>={minimum_fact_adjacent}, expected_before={expected_fact_adjacent}, "
f"actual_after={fact_adjacent}."
)
planar_relation_summary = self._face_push_pull_planar_relation_result_summary_or_raise(
plan,
matched_face_id,
facts,
)
first_level_summary = (
" First-level check: "
f"boundary_edges={boundary_edges}, boundary_vertices={boundary_vertices}, "
f"adjacent_faces={adjacent_faces}, inner_wires={inner_boundary_wires}; "
f"{area_summary}"
f"fact_subject_faces={fact_subject}, fact_included_faces={fact_included}."
f"{planar_relation_summary}"
)
return self._face_edit_result_summary(plan, check) + first_level_summary
@@ -10183,8 +10350,9 @@ class OperationMixin:
raise RuntimeError(f"{transform_kind} edge-length transform produced an empty shape.")
return result
def push_pull_face(self, face_id: int, distance: float) -> str:
plan = self.push_pull_plan(face_id, distance)
def push_pull_face(self, face_id: int, distance: float, *, plan: dict[str, object] | None = None) -> str:
if plan is None:
plan = self.push_pull_plan(face_id, distance)
if plan["status"] == "blocked":
raise ValueError(str(plan["message"]))
face = self.faces[face_id]
@@ -10434,6 +10602,16 @@ class OperationMixin:
f"risk={plan['risk']}. {result_check}"
)
def push_pull_face_keep_relations(self, face_id: int, distance: float) -> str:
plan = self.push_pull_keep_relations_plan(face_id, distance)
if plan["status"] == "blocked":
raise ValueError(str(plan["message"]))
result = self.push_pull_face(face_id, distance, plan=plan)
return (
f"{result} "
f"Keep-relation intent: {plan.get('face_push_pull_planar_constraint_summary', '')}"
)
def _cylindrical_cap_local_shell_rebuild_shape(
self,
face_id: int,
+26
View File
@@ -91,6 +91,19 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
"first_level_planar_relation_observed_modes",
"first_level_planar_relation_summary",
"first_level_planar_relation_note",
"face_push_pull_planar_relation_constraint_requested",
"face_push_pull_planar_relation_constraint_label",
"face_push_pull_planar_constraint_status",
"face_push_pull_planar_constraint_summary",
"face_push_pull_planar_constraint_relation_count",
"face_push_pull_planar_constraint_parallel_count",
"face_push_pull_planar_constraint_perpendicular_count",
"face_push_pull_planar_constraint_angled_count",
"face_push_pull_planar_constraint_adjacent_face_count",
"face_push_pull_planar_constraint_non_planar_face_ids",
"face_push_pull_planar_constraint_observed_modes",
"face_push_pull_planar_constraint_blockers",
"face_push_pull_planar_constraint_warnings",
"edge_length_planar_relation_constraint_requested",
"edge_length_planar_relation_constraint_label",
"edge_length_planar_constraint_status",
@@ -566,6 +579,19 @@ INFO_LABELS = {
"first_level_planar_relation_observed_modes": "已识别平面关系",
"first_level_planar_relation_summary": "一级平面关系摘要",
"first_level_planar_relation_note": "一级平面关系说明",
"face_push_pull_planar_relation_constraint_requested": "Face保持关系已请求",
"face_push_pull_planar_relation_constraint_label": "Face保持关系约束",
"face_push_pull_planar_constraint_status": "Face保持关系状态",
"face_push_pull_planar_constraint_summary": "Face保持关系摘要",
"face_push_pull_planar_constraint_relation_count": "Face保持关系数",
"face_push_pull_planar_constraint_parallel_count": "Face保持平行关系数",
"face_push_pull_planar_constraint_perpendicular_count": "Face保持垂直关系数",
"face_push_pull_planar_constraint_angled_count": "Face保持斜交关系数",
"face_push_pull_planar_constraint_adjacent_face_count": "Face保持相邻面数",
"face_push_pull_planar_constraint_non_planar_face_ids": "Face保持非平面相邻面",
"face_push_pull_planar_constraint_observed_modes": "Face保持已识别关系",
"face_push_pull_planar_constraint_blockers": "Face保持关系阻止原因",
"face_push_pull_planar_constraint_warnings": "Face保持关系警告",
"edge_length_planar_relation_constraint_requested": "已请求保持一级平面关系",
"edge_length_planar_relation_constraint_label": "一级平面关系约束",
"edge_length_planar_constraint_status": "端面推拉约束状态",
+80 -6
View File
@@ -347,6 +347,12 @@ class WindowActionMixin:
self.statusBar().showMessage("未选择可修复对象")
def push_pull_face(self) -> None:
self._push_pull_face_action(keep_relations=False)
def push_pull_face_keep_relations(self) -> None:
self._push_pull_face_action(keep_relations=True)
def _push_pull_face_action(self, *, keep_relations: bool) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再执行新的拉伸/切除。"):
@@ -360,10 +366,11 @@ class WindowActionMixin:
QMessageBox.critical(self, "距离无效", "请输入数字形式的面移动距离。")
return
face_id = self.selected_face_id
plan = self._push_pull_plan_for_action(face_id, distance)
plan = self._push_pull_plan_for_action(face_id, distance, keep_relations=keep_relations)
operation_name = "拉伸/切除平面(保持关系)" if keep_relations else "拉伸/切除平面"
if plan["status"] == "blocked":
self._show_blocked_plan_message("拉伸/切除平面", plan, "拉伸/切除平面已阻止")
self._show_blocked_plan_message(operation_name, plan, "拉伸/切除平面已阻止")
return
if plan["risk"] != "low":
warnings = str(plan.get("warnings", ""))
@@ -383,6 +390,7 @@ class WindowActionMixin:
f"目标法向位置: {_format_value(plan.get('target_plane_position'))}\n"
f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n"
f"执行影响: {_format_value(plan.get('edit_semantics'))}\n"
f"关系约束: {_format_value(plan.get('face_push_pull_planar_constraint_summary', ''))}\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
@@ -395,18 +403,23 @@ class WindowActionMixin:
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage("已取消拉伸/切除平面")
return
isolation = self._isolation_for_plan(plan, "push_pull_face", [face_id, distance])
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:
self.clear_edit_preview(render=False)
def action():
if keep_relations:
return self.model.push_pull_face_keep_relations(face_id, distance)
return self.model.push_pull_face(face_id, distance)
self._run_edit_action(
action,
operation_name="拉伸/切除平面",
operation_name=operation_name,
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
@@ -449,6 +462,31 @@ class WindowActionMixin:
"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"),
"face_push_pull_planar_relation_constraint_requested": plan.get(
"face_push_pull_planar_relation_constraint_requested"
),
"face_push_pull_planar_relation_constraint_label": plan.get(
"face_push_pull_planar_relation_constraint_label"
),
"face_push_pull_planar_constraint_status": plan.get("face_push_pull_planar_constraint_status"),
"face_push_pull_planar_constraint_summary": plan.get("face_push_pull_planar_constraint_summary"),
"face_push_pull_planar_constraint_relation_count": plan.get(
"face_push_pull_planar_constraint_relation_count"
),
"face_push_pull_planar_constraint_parallel_count": plan.get(
"face_push_pull_planar_constraint_parallel_count"
),
"face_push_pull_planar_constraint_perpendicular_count": plan.get(
"face_push_pull_planar_constraint_perpendicular_count"
),
"face_push_pull_planar_constraint_angled_count": plan.get(
"face_push_pull_planar_constraint_angled_count"
),
"face_push_pull_planar_constraint_adjacent_face_count": plan.get(
"face_push_pull_planar_constraint_adjacent_face_count"
),
"face_push_pull_planar_constraint_blockers": plan.get("face_push_pull_planar_constraint_blockers"),
"face_push_pull_planar_constraint_warnings": plan.get("face_push_pull_planar_constraint_warnings"),
},
target_kind="face",
target_id=face_id,
@@ -700,16 +738,51 @@ class WindowActionMixin:
)
return plan
def _push_pull_plan_for_action(self, face_id: int, distance: float) -> dict[str, object]:
def _push_pull_plan_for_action(
self,
face_id: int,
distance: float,
*,
keep_relations: bool = False,
) -> 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):
if keep_relations:
blocked_plan = dict(quick_plan)
message = (
"当前 Face 属于复杂大 STEP 的多边界平面区域;“保持关系”需要在界面提交前完成一级平面关系验证。"
"为避免长时间卡住,当前版本暂不支持这种组合。请先选择简单平面 Face,"
"或改用普通“拉伸/切除”。"
)
blocked_plan.update(
{
"status": "blocked",
"risk": "blocked",
"message": message,
"blockers": message,
"resize_strategy": "push-pull-planar-face-keep-relations",
"edit_strategy_label": "保持一级平面关系的拉伸/切除",
"edit_semantics": (
"沿当前 Face 法向拉伸/切除,同时要求一级相邻平面关系可验证。"
),
"face_push_pull_planar_relation_constraint_requested": True,
"face_push_pull_planar_relation_constraint_label": "保持一级平面关系",
"face_push_pull_planar_constraint_status": "blocked",
"face_push_pull_planar_constraint_summary": "Face保持关系:当前复杂多边界平面需要完整验证,暂未开放。",
"face_push_pull_planar_constraint_blockers": message,
}
)
return blocked_plan
return self._deferred_push_pull_model_plan(quick_plan)
try:
model_plan = self.model.push_pull_plan(face_id, distance)
if keep_relations:
model_plan = self.model.push_pull_keep_relations_plan(face_id, distance)
else:
model_plan = self.model.push_pull_plan(face_id, distance)
except Exception as exc:
blocked_plan = dict(quick_plan)
blocked_plan.update(
@@ -1453,6 +1526,7 @@ class WindowActionMixin:
risk = str(plan.get("risk", ""))
isolated_geometry_operations = {
"push_pull_face",
"push_pull_face_keep_relations",
"move_face_plane_offset_local",
"resize_face_area_local",
"resize_face_area",
+18
View File
@@ -3594,6 +3594,24 @@ class WindowStateMixin:
"target_transform": "plane_target_position_to_offset",
"transform_context": {"current_plane_position": current_plane_position},
},
"keep_relations": {
"label": "保持关系",
"action": "push_pull_face_keep_relations",
"target_attr": "offset_input",
"enabled": current_plane_position is not None,
"enabled_tip": (
"输入偏移的目标位置;程序会沿当前 Face 法向拉伸/切除,"
"并要求一级相邻平面的平行/垂直关系可验证。"
),
"disabled_tip": "当前平面缺少稳定移动方向或基准点,不能保持一级平面关系。",
"range_hint": (
"这是带约束守门的拉伸/切除:只支持直接相邻平面关系清楚、且关系为平行/垂直的一级邻域;"
"遇到非平面相邻面、斜交关系或复杂多边界慢计划会直接阻止。"
f"{face_offset_hard_limit}"
),
"target_transform": "plane_target_position_to_offset",
"transform_context": {"current_plane_position": current_plane_position},
},
"local": {
"label": "局部重建",
"action": "move_selected_face_plane_position_local",