feat: 增加Face偏移保持一级关系策略
This commit is contained in:
+180
-2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user