diff --git a/README.md b/README.md index 5075757..89c0807 100644 --- a/README.md +++ b/README.md @@ -291,7 +291,7 @@ STEP/B-Rep 参数化编辑主线 │ ├── [x] [平行/垂直 -> Face/Edge 一级事实识别] │ │ └── Face 和 Edge 一级事实图会记录直接邻域内的平行、垂直、斜交关系数量和摘要,供 UI 说明、计划守门和后续约束求解复用。 │ ├── [~] [平行/垂直 -> 通用可选约束] -│ │ └── Face 偏移/拉伸和 Edge 长度已开放受限的“保持关系”用户策略,会要求一级平面关系可验证并做结果反查;跨多面、跨特征的通用约束求解仍需继续泛化。 +│ │ └── Face 偏移可走受限拉伸/切除,Face 面内长度/宽度的“保持关系”会转成所属对象单向缩放,Face 中心会转成所属对象平移,Edge 长度可走端面推拉;这些路径都会要求一级平面关系可验证并做结果反查。跨多面、跨特征的通用约束求解仍需继续泛化。 │ ├── [x] [复杂曲线 Edge -> 可编辑入口守门] │ │ └── B-spline / Bezier / 其它复杂曲线不再在扫描列表或参数表里伪装成稳定“改长度”入口。 │ └── [ ] [任意曲线 Edge -> 通用约束编辑] diff --git a/scripts/verify_face_edit_suite.py b/scripts/verify_face_edit_suite.py index 288e506..3daa704 100644 --- a/scripts/verify_face_edit_suite.py +++ b/scripts/verify_face_edit_suite.py @@ -14,6 +14,10 @@ CASES: tuple[tuple[str, tuple[str, ...]], ...] = ( "face width, current face only", ("verify_face_resize_semantics.py", "--strategy", "local", "--axis", "width", "--target-size", "15"), ), + ( + "face width, keep first-level planar relations", + ("verify_face_resize_semantics.py", "--strategy", "keep_relations", "--axis", "width", "--target-size", "15"), + ), ( "Face first-level shared-edge topology", ("verify_face_first_level_topology.py",), @@ -30,6 +34,10 @@ CASES: tuple[tuple[str, tuple[str, ...]], ...] = ( "face height, current face only", ("verify_face_resize_semantics.py", "--strategy", "local", "--axis", "height", "--target-size", "15"), ), + ( + "face height, keep first-level planar relations", + ("verify_face_resize_semantics.py", "--strategy", "keep_relations", "--axis", "height", "--target-size", "15"), + ), ( "face height, owning feature", ("verify_face_resize_semantics.py", "--strategy", "owning", "--axis", "height", "--target-size", "15"), @@ -50,6 +58,10 @@ CASES: tuple[tuple[str, tuple[str, ...]], ...] = ( "face center, current face only", ("verify_face_resize_semantics.py", "--property", "center", "--strategy", "local", "--center-offset", "2,0,3"), ), + ( + "face center, keep first-level planar relations", + ("verify_face_resize_semantics.py", "--property", "center", "--strategy", "keep_relations", "--center-offset", "2,0,3"), + ), ( "face center, owning feature", ("verify_face_resize_semantics.py", "--property", "center", "--strategy", "owning", "--center-offset", "2,0,3"), diff --git a/scripts/verify_face_resize_semantics.py b/scripts/verify_face_resize_semantics.py index 137b8e3..bdf317f 100644 --- a/scripts/verify_face_resize_semantics.py +++ b/scripts/verify_face_resize_semantics.py @@ -15,12 +15,15 @@ DEFAULT_MODEL = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step" FACE_CASES = ( ("size", "local", "width"), + ("size", "keep_relations", "width"), ("size", "owning", "width"), ("size", "local", "height"), + ("size", "keep_relations", "height"), ("size", "owning", "height"), ("area", "local", "width"), ("area", "owning", "width"), ("center", "local", "width"), + ("center", "keep_relations", "width"), ("center", "owning", "width"), ("offset", "local", "width"), ("offset", "owning", "width"), @@ -183,13 +186,19 @@ def main() -> int: before = model.stats() current_info = model.face_info(face_id) - if args.strategy in {"push_pull", "keep_relations"} and args.property != "offset": - raise SystemExit("--strategy push_pull/keep_relations is only valid with --property offset") + if args.strategy == "push_pull" and args.property != "offset": + raise SystemExit("--strategy push_pull is only valid with --property offset") + if args.strategy == "keep_relations" and args.property not in {"size", "center", "offset"}: + raise SystemExit("--strategy keep_relations is only valid with --property size/center/offset") if args.property == "size" and args.strategy == "local": plan = model.face_size_local_resize_plan(face_id, args.target_size, args.axis) result = model.resize_face_size_local(face_id, args.target_size, args.axis) expected_strategy = f"local-face-{args.axis}-only-deform" + elif args.property == "size" and args.strategy == "keep_relations": + plan = model.face_size_local_resize_keep_relations_plan(face_id, args.target_size, args.axis) + result = model.resize_face_size_local_keep_relations(face_id, args.target_size, args.axis) + expected_strategy = f"axis-scale-owning-shape-from-face-{args.axis}" elif args.property == "size": plan = model.face_size_owning_scale_plan(face_id, args.target_size, args.axis) result = model.resize_face_size_owning_scale(face_id, args.target_size, args.axis) @@ -202,7 +211,7 @@ def main() -> int: plan = model.face_area_scale_plan(face_id, args.target_area) result = model.resize_face_area(face_id, args.target_area) expected_strategy = "uniform-scale-face-area-fallback" - elif args.property == "center" and args.strategy == "local": + elif args.property == "center" and args.strategy in {"local", "keep_relations"}: current_center = current_info.get("area_center") or current_info.get("bbox_center") if not isinstance(current_center, tuple) or len(current_center) != 3: raise SystemExit("selected Face does not have a stable center") @@ -212,9 +221,14 @@ def main() -> int: float(current_center[1]) + offset[1], float(current_center[2]) + offset[2], ) - plan = model.face_center_local_move_plan(face_id, target_center) - result = model.move_face_center_local(face_id, target_center) - expected_strategy = "local-face-only-deform" + if args.strategy == "keep_relations": + plan = model.face_center_local_move_keep_relations_plan(face_id, target_center) + result = model.move_face_center_local_keep_relations(face_id, target_center) + expected_strategy = "translate-owning-shape-from-face-center" + else: + plan = model.face_center_local_move_plan(face_id, target_center) + result = model.move_face_center_local(face_id, target_center) + expected_strategy = "local-face-only-deform" elif args.property == "center": current_center = current_info.get("area_center") or current_info.get("bbox_center") if not isinstance(current_center, tuple) or len(current_center) != 3: @@ -298,7 +312,7 @@ def main() -> int: resolved_strategy = "push-pull-planar-face" if resolved_strategy != expected_strategy: raise SystemExit(f"expected {expected_strategy}, got {resolved_strategy or ''}") - if args.property == "offset" and args.strategy == "keep_relations": + if args.strategy == "keep_relations": if plan.get("face_push_pull_planar_relation_constraint_requested") is not True: raise SystemExit(f"keep-relations plan should record requested relation constraint: {plan}") if plan.get("face_push_pull_planar_constraint_status") != "ready": diff --git a/scripts/verify_property_editor_specs.py b/scripts/verify_property_editor_specs.py index 1b63d28..c45752b 100644 --- a/scripts/verify_property_editor_specs.py +++ b/scripts/verify_property_editor_specs.py @@ -498,6 +498,27 @@ def main() -> int: raise SystemExit(f"Face plane-offset keep-relations scope should use its own action: {offset_keep_relations}") if not bool(offset_keep_relations.get("enabled", False)): raise SystemExit(f"Face plane-offset keep-relations scope should be available on simple planar Face: {offset_keep_relations}") + width_keep_relations = _scope_mode(plane_specs, "local_face_width", "keep_relations") + if str(width_keep_relations.get("label") or "") != "保持关系": + raise SystemExit(f"Face width keep-relations scope should be labelled clearly: {width_keep_relations}") + if str(width_keep_relations.get("action") or "") != "resize_face_width_keep_relations": + raise SystemExit(f"Face width keep-relations scope should use its own action: {width_keep_relations}") + if not bool(width_keep_relations.get("enabled", False)): + raise SystemExit(f"Face width keep-relations scope should be available on simple planar Face: {width_keep_relations}") + height_keep_relations = _scope_mode(plane_specs, "local_face_height", "keep_relations") + if str(height_keep_relations.get("label") or "") != "保持关系": + raise SystemExit(f"Face height keep-relations scope should be labelled clearly: {height_keep_relations}") + if str(height_keep_relations.get("action") or "") != "resize_face_height_keep_relations": + raise SystemExit(f"Face height keep-relations scope should use its own action: {height_keep_relations}") + if not bool(height_keep_relations.get("enabled", False)): + raise SystemExit(f"Face height keep-relations scope should be available on simple planar Face: {height_keep_relations}") + center_keep_relations = _scope_mode(plane_specs, "face_center_position", "keep_relations") + if str(center_keep_relations.get("label") or "") != "保持关系": + raise SystemExit(f"Face center keep-relations scope should be labelled clearly: {center_keep_relations}") + if str(center_keep_relations.get("action") or "") != "move_selected_face_center_keep_relations": + raise SystemExit(f"Face center keep-relations scope should use its own action: {center_keep_relations}") + if not bool(center_keep_relations.get("enabled", False)): + raise SystemExit(f"Face center keep-relations scope should be available on simple planar Face: {center_keep_relations}") _assert_scoped_hint_fragments( plane_specs, "face_target_normal_position", @@ -581,7 +602,7 @@ def main() -> int: if "push_pull_distance" in plane_keys: raise SystemExit("plane Face should not expose a separate push_pull_distance row") for key in ("local_face_width", "local_face_height"): - for mode in ("local", "owning"): + for mode in ("local", "keep_relations", "owning"): _assert_scoped_hint_fragments( plane_specs, key, @@ -595,7 +616,7 @@ def main() -> int: mode, ("会被阻止",), ) - for mode in ("local", "owning"): + for mode in ("local", "keep_relations", "owning"): _assert_scoped_hint_fragments( plane_specs, "face_center_position", diff --git a/step_editor/isolated_edit_worker.py b/step_editor/isolated_edit_worker.py index 8a41220..30182e6 100644 --- a/step_editor/isolated_edit_worker.py +++ b/step_editor/isolated_edit_worker.py @@ -34,6 +34,8 @@ def _execute(model: StepModel, operation: str, args: list[object]) -> str: return model.resize_face_area(int(args[0]), float(args[1])) if operation == "resize_face_size_local": return model.resize_face_size_local(int(args[0]), float(args[1]), str(args[2])) + if operation == "resize_face_size_local_keep_relations": + return model.resize_face_size_local_keep_relations(int(args[0]), float(args[1]), str(args[2])) if operation == "resize_face_size_owning_scale": return model.resize_face_size_owning_scale(int(args[0]), float(args[1]), str(args[2])) if operation == "move_face_center_local": @@ -44,6 +46,14 @@ def _execute(model: StepModel, operation: str, args: list[object]) -> str: int(args[0]), (float(center[0]), float(center[1]), float(center[2])), ) + if operation == "move_face_center_local_keep_relations": + center = list(args[1]) + if len(center) != 3: + raise ValueError("move_face_center_local_keep_relations requires a 3D target center.") + return model.move_face_center_local_keep_relations( + int(args[0]), + (float(center[0]), float(center[1]), float(center[2])), + ) if operation == "resize_shell_thickness": return model.resize_shell_thickness(int(args[0]), float(args[1])) if operation == "resize_shell_thickness_owning_scale": diff --git a/step_editor/operations.py b/step_editor/operations.py index a52b851..87a456a 100644 --- a/step_editor/operations.py +++ b/step_editor/operations.py @@ -262,6 +262,8 @@ class OperationMixin: self, face_id: int, plan: dict[str, object], + *, + operation_label: str = "拉伸/切除", ) -> 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 "") @@ -313,10 +315,13 @@ class OperationMixin: ) else: summary = ( - "Face保持关系:可按拉伸/切除执行并反查一级平面关系;" + f"Face保持关系:可按{operation_label}执行并反查一级平面关系;" f"平行 {parallel_count},垂直 {perpendicular_count}。" ) return { + "face_planar_relation_constraint_requested": True, + "face_planar_relation_constraint_label": "保持一级平面关系", + "face_planar_relation_constraint_operation": operation_label, "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", @@ -339,16 +344,23 @@ class OperationMixin: ), } - 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 与共享边一级相邻平面的平行/垂直关系" - "在执行前可验证,并在执行后反查。" + def _with_face_planar_relation_constraint( + self, + face_id: int, + plan: dict[str, object], + *, + operation_label: str, + edit_strategy_label: str, + edit_semantics: str, + ) -> dict[str, object]: + constraint_fields = self._face_push_pull_planar_relation_constraint_fields( + face_id, + plan, + operation_label=operation_label, ) + plan.update(constraint_fields) + plan["edit_strategy_label"] = edit_strategy_label + plan["edit_semantics"] = edit_semantics 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" @@ -368,6 +380,21 @@ class OperationMixin: plan["message"] = str(constraint_fields["face_push_pull_planar_constraint_summary"]) return plan + def push_pull_keep_relations_plan(self, face_id: int, distance: float) -> dict[str, object]: + plan = self.push_pull_plan(face_id, distance) + self._with_face_planar_relation_constraint( + face_id, + plan, + operation_label="拉伸/切除", + edit_strategy_label="保持一级平面关系的拉伸/切除", + edit_semantics=( + "沿当前 Face 法向拉伸/切除,同时要求当前 Face 与共享边一级相邻平面的平行/垂直关系" + "在执行前可验证,并在执行后反查。" + ), + ) + plan["resize_strategy"] = "push-pull-planar-face-keep-relations" + return plan + def _multistep_prismatic_plan_blocker(self, face_id: int, operation_label: str) -> str: try: quick_info = self.quick_face_info(face_id) @@ -4070,6 +4097,23 @@ class OperationMixin: ), } + def face_center_local_move_keep_relations_plan( + self, + face_id: int, + target_center: tuple[float, float, float], + ) -> dict[str, object]: + plan = self.face_center_owning_translation_plan(face_id, target_center) + return self._with_face_planar_relation_constraint( + face_id, + plan, + operation_label="中心移动", + edit_strategy_label="保持一级平面关系的中心移动", + edit_semantics=( + "把目标中心换算成平移量并移动所属特征或 Solid,同时要求当前 Face 与共享边一级相邻平面的" + "平行/垂直关系在执行前可验证,并在执行后反查;不做单面局部扭曲。" + ), + ) + def existing_chamfer_resize_plan(self, face_id: int, target_distance: float) -> dict[str, object]: if face_id < 0 or face_id >= len(self.faces): raise ValueError(f"Unknown face id {face_id}") @@ -4604,6 +4648,26 @@ class OperationMixin: ), } + def face_size_local_resize_keep_relations_plan( + self, + face_id: int, + target_size: float, + axis: str = "width", + ) -> dict[str, object]: + axis_key = "height" if str(axis).strip().lower() in {"height", "h", "v", "y"} else "width" + axis_label = "面内宽度" if axis_key == "height" else "面内长度" + plan = self.face_size_owning_scale_plan(face_id, target_size, axis_key) + return self._with_face_planar_relation_constraint( + face_id, + plan, + operation_label=f"{axis_label}修改", + edit_strategy_label=f"保持一级平面关系的{axis_label}修改", + edit_semantics=( + f"沿当前 Face 的{axis_label}方向缩放所属特征或 Solid,同时要求当前 Face 与共享边一级相邻平面的" + "平行/垂直关系在执行前可验证,并在执行后反查;不做单面局部扭曲。" + ), + ) + def _rectangular_prismatic_size_resize_plan( self, *, @@ -8144,6 +8208,35 @@ class OperationMixin: f"risk={plan['risk']}. {result_check}" ) + def move_face_center_local_keep_relations( + self, + face_id: int, + target_center: tuple[float, float, float], + ) -> str: + plan = self.face_center_local_move_keep_relations_plan(face_id, target_center) + if plan["status"] == "blocked": + raise ValueError(str(plan["message"])) + + self._remember_face_target_logical_id(plan, face_id) + vector = tuple(plan["translation_vector"]) + result, result_check = self._run_checked_face_edit( + plan, + lambda: ( + self.translate_solid(int(plan["solid_id"]), vector) + if plan.get("target_kind") == "solid" + else self.translate_part(int(plan["part_id"]), vector) + ), + ) + return ( + "Face center move completed with first-level planar relations: " + f"face {face_id}, " + f"current_center={plan.get('current_face_center')}, " + f"target_center={plan.get('target_face_center')}, " + f"move={plan.get('face_center_move_vector')}, " + f"target={plan.get('target_kind')}, " + f"risk={plan['risk']}. {result_check} {result}" + ) + def face_area_local_resize_preview_polydata( self, face_id: int, @@ -8243,6 +8336,33 @@ class OperationMixin: f"risk={plan['risk']}. {result_check}" ) + def resize_face_size_local_keep_relations(self, face_id: int, target_size: float, axis: str = "width") -> str: + plan = self.face_size_local_resize_keep_relations_plan(face_id, target_size, axis) + if plan["status"] == "blocked": + raise ValueError(str(plan["message"])) + + self._remember_face_target_logical_id(plan, face_id) + _action_result, result_check = self._run_checked_face_edit( + plan, + lambda: ( + self._apply_local_face_deform(plan) + if plan.get("owning_face_size_rebuild_mode") == "planar-rebuild" + else self._apply_edge_length_affine_transform(plan) + ), + ) + return ( + "Face owning size resize completed with first-level planar relations: " + f"face {face_id}, " + f"axis={plan.get('face_size_axis')}, " + f"current_size={float(plan['current_face_size']):g}, " + f"target_size={float(plan['target_face_size']):g}, " + f"delta={float(plan['face_size_delta']):g}, " + f"scale={float(plan['face_size_scale']):g}, " + f"rebuild_mode={plan.get('owning_face_size_rebuild_mode')}, " + f"target={plan.get('affine_target_kind')}, " + f"risk={plan['risk']}. {result_check}" + ) + def face_size_owning_scale_preview_polydata( self, face_id: int, @@ -8318,7 +8438,10 @@ class OperationMixin: matched_face_id: int, facts: dict[str, object], ) -> str: - if plan.get("face_push_pull_planar_relation_constraint_requested") is not True: + if ( + plan.get("face_planar_relation_constraint_requested") is not True + and 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.") diff --git a/step_editor/window_actions.py b/step_editor/window_actions.py index e6ff8de..6eaead0 100644 --- a/step_editor/window_actions.py +++ b/step_editor/window_actions.py @@ -1507,10 +1507,13 @@ class WindowActionMixin: "面面积(缩放特征)", "面内长度(局部重建)", "面内宽度(局部重建)", + "面内长度(保持关系)", + "面内宽度(保持关系)", "面内长度(缩放特征)", "面内宽度(缩放特征)", "偏移(局部重建)", "中心(局部重建)", + "中心(保持关系)", "圆柱高度调整", "高度(缩放特征)缩放所属对象", } @@ -1531,8 +1534,10 @@ class WindowActionMixin: "resize_face_area_local", "resize_face_area", "resize_face_size_local", + "resize_face_size_local_keep_relations", "resize_face_size_owning_scale", "move_face_center_local", + "move_face_center_local_keep_relations", "resize_shell_thickness", "resize_shell_thickness_owning_scale", "resize_cylindrical_height", @@ -5920,10 +5925,16 @@ class WindowActionMixin: ) def resize_face_width_local(self) -> None: - self._resize_face_size_local("width") + self._resize_face_size_local("width", keep_relations=False) def resize_face_height_local(self) -> None: - self._resize_face_size_local("height") + self._resize_face_size_local("height", keep_relations=False) + + def resize_face_width_keep_relations(self) -> None: + self._resize_face_size_local("width", keep_relations=True) + + def resize_face_height_keep_relations(self) -> None: + self._resize_face_size_local("height", keep_relations=True) def resize_face_width_owning_scale(self) -> None: self._resize_face_size_owning_scale("width") @@ -5931,12 +5942,13 @@ class WindowActionMixin: def resize_face_height_owning_scale(self) -> None: self._resize_face_size_owning_scale("height") - def _resize_face_size_local(self, axis: str) -> None: + def _resize_face_size_local(self, axis: str, *, keep_relations: bool) -> None: if self.model is None: return axis_key = "height" if str(axis).lower() == "height" else "width" axis_label = "面内宽度" if axis_key == "height" else "面内长度" - if self._edit_busy(f"请等待当前编辑完成后再修改{axis_label}(局部重建)。"): + mode_label = f"{axis_label}(保持关系)" if keep_relations else f"{axis_label}(局部重建)" + if self._edit_busy(f"请等待当前编辑完成后再修改{mode_label}。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。") @@ -5953,10 +5965,13 @@ class WindowActionMixin: face_id = self.selected_face_id 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) + if keep_relations: + plan = self.model.face_size_local_resize_keep_relations_plan(face_id, target_size, axis_key) + else: + plan = self.model.face_size_local_resize_plan(face_id, target_size, axis_key) lines = [ f"Face: {face_id}", - f"修改对象: {axis_label}(局部重建)", + f"修改对象: {mode_label}", f"当前面内长度: {_format_value(plan.get('current_face_width'))}", f"目标面内长度: {_format_value(plan.get('target_face_width'))}", f"当前面内宽度: {_format_value(plan.get('current_face_height'))}", @@ -5970,21 +5985,23 @@ class WindowActionMixin: f"影响目标: {_format_value(plan.get('local_face_deform_target_kind'))}", ] if not self._confirm_quick_edit_plan( - f"{axis_label}(局部重建)", + mode_label, plan, lines, - f"{axis_label}(局部重建)已阻止", - f"已取消{axis_label}(局部重建)", + f"{mode_label}已阻止", + f"已取消{mode_label}", ): return self.clear_edit_preview(render=False) def action(): + if keep_relations: + return self.model.resize_face_size_local_keep_relations(face_id, target_size, axis_key) return self.model.resize_face_size_local(face_id, target_size, axis_key) self._run_edit_action( action, - operation_name=f"修改{axis_label}(局部重建)", + operation_name=f"修改{mode_label}", target=f"Face {face_id}", parameters={ "face_id": face_id, @@ -6013,6 +6030,11 @@ class WindowActionMixin: "resize_strategy": plan.get("resize_strategy"), "edit_strategy_label": plan.get("edit_strategy_label"), "edit_semantics": plan.get("edit_semantics"), + "face_planar_relation_constraint_requested": plan.get("face_planar_relation_constraint_requested"), + "face_push_pull_planar_constraint_summary": plan.get("face_push_pull_planar_constraint_summary"), + "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"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), @@ -6022,7 +6044,11 @@ class WindowActionMixin: }, target_kind="face", target_id=face_id, - isolation=self._isolation_for_plan(plan, "resize_face_size_local", [face_id, target_size, axis_key]), + isolation=self._isolation_for_plan( + plan, + "resize_face_size_local_keep_relations" if keep_relations else "resize_face_size_local", + [face_id, target_size, axis_key], + ), ) def _resize_face_size_owning_scale(self, axis: str) -> None: @@ -6361,9 +6387,16 @@ class WindowActionMixin: ) def move_selected_face_center_local(self) -> None: + self._move_selected_face_center_local(keep_relations=False) + + def move_selected_face_center_keep_relations(self) -> None: + self._move_selected_face_center_local(keep_relations=True) + + def _move_selected_face_center_local(self, *, keep_relations: bool) -> None: if self.model is None: return - if self._edit_busy("请等待当前编辑完成后再执行中心(局部重建)。"): + mode_label = "中心(保持关系)" if keep_relations else "中心(局部重建)" + if self._edit_busy(f"请等待当前编辑完成后再执行{mode_label}。"): return if self.selected_face_id is None: QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。") @@ -6378,9 +6411,9 @@ class WindowActionMixin: current_center = self._selected_face_center_for_translation(face_id) if current_center is None: self._show_immediate_cannot_edit_message( - "中心(局部重建)", + mode_label, "当前 Face 缺少稳定中心坐标。", - "中心(局部重建)已阻止", + f"{mode_label}已阻止", ) return target_center = ( @@ -6390,7 +6423,10 @@ class WindowActionMixin: ) 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) + if keep_relations: + plan = self.model.face_center_local_move_keep_relations_plan(face_id, target_center) + else: + 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'))}", @@ -6403,21 +6439,23 @@ class WindowActionMixin: f"影响目标: {_format_value(plan.get('local_face_deform_target_kind'))}", ] if not self._confirm_quick_edit_plan( - "中心(局部重建)", + mode_label, plan, lines, - "中心(局部重建)已阻止", - "已取消中心(局部重建)", + f"{mode_label}已阻止", + f"已取消{mode_label}", ): return self.clear_edit_preview(render=False) def action(): + if keep_relations: + return self.model.move_face_center_local_keep_relations(face_id, target_center) return self.model.move_face_center_local(face_id, target_center) self._run_edit_action( action, - operation_name="中心(局部重建)", + operation_name=mode_label, target=f"Face {face_id}", parameters={ "face_id": face_id, @@ -6436,6 +6474,11 @@ class WindowActionMixin: "resize_strategy": plan.get("resize_strategy"), "edit_strategy_label": plan.get("edit_strategy_label"), "edit_semantics": plan.get("edit_semantics"), + "face_planar_relation_constraint_requested": plan.get("face_planar_relation_constraint_requested"), + "face_push_pull_planar_constraint_summary": plan.get("face_push_pull_planar_constraint_summary"), + "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"), "resize_status": plan.get("status"), "resize_risk": plan.get("risk"), "resize_message": plan.get("message"), @@ -6445,7 +6488,11 @@ class WindowActionMixin: }, target_kind="face", target_id=face_id, - isolation=self._isolation_for_plan(plan, "move_face_center_local", [face_id, list(target_center)]), + isolation=self._isolation_for_plan( + plan, + "move_face_center_local_keep_relations" if keep_relations else "move_face_center_local", + [face_id, list(target_center)], + ), ) def move_selected_axis_center_by_translation(self) -> None: diff --git a/step_editor/window_state.py b/step_editor/window_state.py index 6f79584..1e9e224 100644 --- a/step_editor/window_state.py +++ b/step_editor/window_state.py @@ -3427,6 +3427,43 @@ class WindowStateMixin: _triple_or_none(action_info.get("area_center")) or _triple_or_none(action_info.get("bbox_center")) ) + keep_relation_fields_present = any( + key in action_info + for key in ( + "first_level_planar_relation_status", + "first_level_planar_relation_count", + "first_level_planar_relation_angled_count", + "first_level_planar_relation_non_planar_face_ids", + "first_level_adjacent_surface_types", + ) + ) + face_keep_relation_enabled = bool(is_plane) + face_keep_relation_disabled_tip = "当前对象不是平面 Face,不能保持一级平面关系。" + if face_keep_relation_enabled and keep_relation_fields_present: + relation_status = str(action_info.get("first_level_planar_relation_status") or "") + relation_count = _int_or_none(action_info.get("first_level_planar_relation_count")) or 0 + angled_count = _int_or_none(action_info.get("first_level_planar_relation_angled_count")) or 0 + non_planar_face_ids = _int_values(action_info.get("first_level_planar_relation_non_planar_face_ids")) + adjacent_surfaces = tuple(action_info.get("first_level_adjacent_surface_types") or ()) + non_plane_adjacent_surfaces = tuple( + item + for item in adjacent_surfaces + if isinstance(item, (tuple, list)) + and len(item) >= 2 + and str(item[1]) != "plane" + ) + if non_planar_face_ids or non_plane_adjacent_surfaces: + face_keep_relation_enabled = False + face_keep_relation_disabled_tip = "一级相邻 Face 中包含非平面/曲面,当前“保持关系”只支持平面邻域。" + elif angled_count > 0: + face_keep_relation_enabled = False + face_keep_relation_disabled_tip = "一级相邻平面中存在斜交关系,当前“保持关系”只支持平行/垂直。" + elif relation_status != "ready": + face_keep_relation_enabled = False + face_keep_relation_disabled_tip = "当前一级平面关系还不可验证,不能使用“保持关系”。" + elif relation_count <= 0: + face_keep_relation_enabled = False + face_keep_relation_disabled_tip = "当前一级邻域没有识别到可保持的平行/垂直平面关系。" if is_plane: current_face_width = _float_or_none(action_info.get("local_face_width")) current_face_height = _float_or_none(action_info.get("local_face_height")) @@ -3461,6 +3498,31 @@ class WindowStateMixin: ), "range_hint": f"{face_size_tip} {relative_range_hint(current_face_width, 0.25, 0.8, face_linear_hard_limit)}", }, + "keep_relations": { + "label": "保持关系", + "action": "resize_face_width_keep_relations", + "target_attr": "face_width_input", + "enabled": bool( + current_face_width is not None + and current_face_width > 0 + and current_face_center is not None + and (self.selected_part_id is not None or self.selected_solid_id is not None) + and face_keep_relation_enabled + ), + "enabled_tip": ( + "输入目标面内长度;程序会沿该方向缩放所属特征或 Solid," + "并要求一级相邻平面的平行/垂直关系在执行前可验证、执行后可反查。" + ), + "disabled_tip": ( + face_keep_relation_disabled_tip + if not face_keep_relation_enabled + else "当前 Face 缺少稳定面内长度、中心坐标或所属对象,不能保持关系地修改面内长度。" + ), + "range_hint": ( + "保持关系会把面内长度修改转换为所属对象单向缩放,并在结果上反查一级平面关系;" + f"{relative_range_hint(current_face_width, 0.25, 0.8, face_linear_hard_limit)}" + ), + }, "owning": { "label": "缩放特征", "action": "resize_face_width_owning_scale", @@ -3503,6 +3565,31 @@ class WindowStateMixin: ), "range_hint": f"{face_size_tip} {relative_range_hint(current_face_height, 0.25, 0.8, face_linear_hard_limit)}", }, + "keep_relations": { + "label": "保持关系", + "action": "resize_face_height_keep_relations", + "target_attr": "face_height_input", + "enabled": bool( + current_face_height is not None + and current_face_height > 0 + and current_face_center is not None + and (self.selected_part_id is not None or self.selected_solid_id is not None) + and face_keep_relation_enabled + ), + "enabled_tip": ( + "输入目标面内宽度;程序会沿该方向缩放所属特征或 Solid," + "并要求一级相邻平面的平行/垂直关系在执行前可验证、执行后可反查。" + ), + "disabled_tip": ( + face_keep_relation_disabled_tip + if not face_keep_relation_enabled + else "当前 Face 缺少稳定面内宽度、中心坐标或所属对象,不能保持关系地修改面内宽度。" + ), + "range_hint": ( + "保持关系会把面内宽度修改转换为所属对象单向缩放,并在结果上反查一级平面关系;" + f"{relative_range_hint(current_face_height, 0.25, 0.8, face_linear_hard_limit)}" + ), + }, "owning": { "label": "缩放特征", "action": "resize_face_height_owning_scale", @@ -3549,6 +3636,33 @@ class WindowStateMixin: "transform_context": {"current_center": current_face_center}, **face_vector_move_limit(current_face_center), }, + "keep_relations": { + "label": "保持关系", + "action": "move_selected_face_center_keep_relations", + "target_attrs": ("translate_x_input", "translate_y_input", "translate_z_input"), + "enabled": bool( + is_plane + and current_face_center is not None + and (self.selected_part_id is not None or self.selected_solid_id is not None) + and face_keep_relation_enabled + ), + "enabled_tip": ( + "输入这个 Face 中心要移动到的目标 X, Y, Z 坐标;程序会平移所属特征或 Solid," + "并要求一级相邻平面的平行/垂直关系在执行前可验证、执行后可反查。" + ), + "disabled_tip": ( + face_keep_relation_disabled_tip + if not face_keep_relation_enabled + else "当前 Face 缺少稳定中心坐标或所属对象,不能保持关系地修改中心位置。" + ), + "range_hint": ( + "保持关系会把中心修改转换为所属对象平移,并在结果上反查一级平面关系;" + f"{translation_hint()} {face_center_hard_limit}" + ), + "target_transform": "target_center_to_translation", + "transform_context": {"current_center": current_face_center}, + **face_vector_move_limit(current_face_center), + }, "owning": { "label": "移动特征", "action": "move_selected_face_center",