feat: 扩展Face保持关系编辑语义

This commit is contained in:
2026-08-10 17:50:50 +08:00
parent 61f0b76e59
commit 3a851d0967
8 changed files with 382 additions and 41 deletions
+1 -1
View File
@@ -291,7 +291,7 @@ STEP/B-Rep 参数化编辑主线
│ ├── [x] [平行/垂直 -> Face/Edge 一级事实识别] │ ├── [x] [平行/垂直 -> Face/Edge 一级事实识别]
│ │ └── Face 和 Edge 一级事实图会记录直接邻域内的平行、垂直、斜交关系数量和摘要,供 UI 说明、计划守门和后续约束求解复用。 │ │ └── Face 和 Edge 一级事实图会记录直接邻域内的平行、垂直、斜交关系数量和摘要,供 UI 说明、计划守门和后续约束求解复用。
│ ├── [~] [平行/垂直 -> 通用可选约束] │ ├── [~] [平行/垂直 -> 通用可选约束]
│ │ └── Face 偏移/拉伸和 Edge 长度已开放受限的“保持关系”用户策略,会要求一级平面关系可验证并做结果反查跨多面、跨特征的通用约束求解仍需继续泛化。 │ │ └── Face 偏移可走受限拉伸/切除,Face 面内长度/宽度的“保持关系”会转成所属对象单向缩放,Face 中心会转成所属对象平移,Edge 长度可走端面推拉;这些路径都会要求一级平面关系可验证并做结果反查跨多面、跨特征的通用约束求解仍需继续泛化。
│ ├── [x] [复杂曲线 Edge -> 可编辑入口守门] │ ├── [x] [复杂曲线 Edge -> 可编辑入口守门]
│ │ └── B-spline / Bezier / 其它复杂曲线不再在扫描列表或参数表里伪装成稳定“改长度”入口。 │ │ └── B-spline / Bezier / 其它复杂曲线不再在扫描列表或参数表里伪装成稳定“改长度”入口。
│ └── [ ] [任意曲线 Edge -> 通用约束编辑] │ └── [ ] [任意曲线 Edge -> 通用约束编辑]
+12
View File
@@ -14,6 +14,10 @@ CASES: tuple[tuple[str, tuple[str, ...]], ...] = (
"face width, current face only", "face width, current face only",
("verify_face_resize_semantics.py", "--strategy", "local", "--axis", "width", "--target-size", "15"), ("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", "Face first-level shared-edge topology",
("verify_face_first_level_topology.py",), ("verify_face_first_level_topology.py",),
@@ -30,6 +34,10 @@ CASES: tuple[tuple[str, tuple[str, ...]], ...] = (
"face height, current face only", "face height, current face only",
("verify_face_resize_semantics.py", "--strategy", "local", "--axis", "height", "--target-size", "15"), ("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", "face height, owning feature",
("verify_face_resize_semantics.py", "--strategy", "owning", "--axis", "height", "--target-size", "15"), ("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", "face center, current face only",
("verify_face_resize_semantics.py", "--property", "center", "--strategy", "local", "--center-offset", "2,0,3"), ("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", "face center, owning feature",
("verify_face_resize_semantics.py", "--property", "center", "--strategy", "owning", "--center-offset", "2,0,3"), ("verify_face_resize_semantics.py", "--property", "center", "--strategy", "owning", "--center-offset", "2,0,3"),
+18 -4
View File
@@ -15,12 +15,15 @@ DEFAULT_MODEL = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step"
FACE_CASES = ( FACE_CASES = (
("size", "local", "width"), ("size", "local", "width"),
("size", "keep_relations", "width"),
("size", "owning", "width"), ("size", "owning", "width"),
("size", "local", "height"), ("size", "local", "height"),
("size", "keep_relations", "height"),
("size", "owning", "height"), ("size", "owning", "height"),
("area", "local", "width"), ("area", "local", "width"),
("area", "owning", "width"), ("area", "owning", "width"),
("center", "local", "width"), ("center", "local", "width"),
("center", "keep_relations", "width"),
("center", "owning", "width"), ("center", "owning", "width"),
("offset", "local", "width"), ("offset", "local", "width"),
("offset", "owning", "width"), ("offset", "owning", "width"),
@@ -183,13 +186,19 @@ def main() -> int:
before = model.stats() before = model.stats()
current_info = model.face_info(face_id) current_info = model.face_info(face_id)
if args.strategy in {"push_pull", "keep_relations"} and args.property != "offset": if args.strategy == "push_pull" and args.property != "offset":
raise SystemExit("--strategy push_pull/keep_relations is only valid with --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": if args.property == "size" and args.strategy == "local":
plan = model.face_size_local_resize_plan(face_id, args.target_size, args.axis) 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) result = model.resize_face_size_local(face_id, args.target_size, args.axis)
expected_strategy = f"local-face-{args.axis}-only-deform" 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": elif args.property == "size":
plan = model.face_size_owning_scale_plan(face_id, args.target_size, args.axis) 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) 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) plan = model.face_area_scale_plan(face_id, args.target_area)
result = model.resize_face_area(face_id, args.target_area) result = model.resize_face_area(face_id, args.target_area)
expected_strategy = "uniform-scale-face-area-fallback" 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") current_center = current_info.get("area_center") or current_info.get("bbox_center")
if not isinstance(current_center, tuple) or len(current_center) != 3: if not isinstance(current_center, tuple) or len(current_center) != 3:
raise SystemExit("selected Face does not have a stable center") raise SystemExit("selected Face does not have a stable center")
@@ -212,6 +221,11 @@ def main() -> int:
float(current_center[1]) + offset[1], float(current_center[1]) + offset[1],
float(current_center[2]) + offset[2], float(current_center[2]) + offset[2],
) )
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) plan = model.face_center_local_move_plan(face_id, target_center)
result = model.move_face_center_local(face_id, target_center) result = model.move_face_center_local(face_id, target_center)
expected_strategy = "local-face-only-deform" expected_strategy = "local-face-only-deform"
@@ -298,7 +312,7 @@ def main() -> int:
resolved_strategy = "push-pull-planar-face" resolved_strategy = "push-pull-planar-face"
if resolved_strategy != expected_strategy: if resolved_strategy != expected_strategy:
raise SystemExit(f"expected {expected_strategy}, got {resolved_strategy or '<none>'}") raise SystemExit(f"expected {expected_strategy}, got {resolved_strategy or '<none>'}")
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: 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}") raise SystemExit(f"keep-relations plan should record requested relation constraint: {plan}")
if plan.get("face_push_pull_planar_constraint_status") != "ready": if plan.get("face_push_pull_planar_constraint_status") != "ready":
+23 -2
View File
@@ -498,6 +498,27 @@ def main() -> int:
raise SystemExit(f"Face plane-offset keep-relations scope should use its own action: {offset_keep_relations}") 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)): 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}") 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( _assert_scoped_hint_fragments(
plane_specs, plane_specs,
"face_target_normal_position", "face_target_normal_position",
@@ -581,7 +602,7 @@ def main() -> int:
if "push_pull_distance" in plane_keys: if "push_pull_distance" in plane_keys:
raise SystemExit("plane Face should not expose a separate push_pull_distance row") raise SystemExit("plane Face should not expose a separate push_pull_distance row")
for key in ("local_face_width", "local_face_height"): 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( _assert_scoped_hint_fragments(
plane_specs, plane_specs,
key, key,
@@ -595,7 +616,7 @@ def main() -> int:
mode, mode,
("会被阻止",), ("会被阻止",),
) )
for mode in ("local", "owning"): for mode in ("local", "keep_relations", "owning"):
_assert_scoped_hint_fragments( _assert_scoped_hint_fragments(
plane_specs, plane_specs,
"face_center_position", "face_center_position",
+10
View File
@@ -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])) return model.resize_face_area(int(args[0]), float(args[1]))
if operation == "resize_face_size_local": if operation == "resize_face_size_local":
return model.resize_face_size_local(int(args[0]), float(args[1]), str(args[2])) 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": if operation == "resize_face_size_owning_scale":
return model.resize_face_size_owning_scale(int(args[0]), float(args[1]), str(args[2])) return model.resize_face_size_owning_scale(int(args[0]), float(args[1]), str(args[2]))
if operation == "move_face_center_local": if operation == "move_face_center_local":
@@ -44,6 +46,14 @@ def _execute(model: StepModel, operation: str, args: list[object]) -> str:
int(args[0]), int(args[0]),
(float(center[0]), float(center[1]), float(center[2])), (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": if operation == "resize_shell_thickness":
return model.resize_shell_thickness(int(args[0]), float(args[1])) return model.resize_shell_thickness(int(args[0]), float(args[1]))
if operation == "resize_shell_thickness_owning_scale": if operation == "resize_shell_thickness_owning_scale":
+134 -11
View File
@@ -262,6 +262,8 @@ class OperationMixin:
self, self,
face_id: int, face_id: int,
plan: dict[str, object], plan: dict[str, object],
*,
operation_label: str = "拉伸/切除",
) -> dict[str, object]: ) -> dict[str, object]:
relation_status = str(plan.get("first_level_planar_relation_status") or "") relation_status = str(plan.get("first_level_planar_relation_status") or "")
relation_scope = str(plan.get("first_level_planar_relation_scope") or "") relation_scope = str(plan.get("first_level_planar_relation_scope") or "")
@@ -313,10 +315,13 @@ class OperationMixin:
) )
else: else:
summary = ( summary = (
"Face保持关系:可按拉伸/切除执行并反查一级平面关系;" f"Face保持关系:可按{operation_label}执行并反查一级平面关系;"
f"平行 {parallel_count},垂直 {perpendicular_count}" f"平行 {parallel_count},垂直 {perpendicular_count}"
) )
return { 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_requested": True,
"face_push_pull_planar_relation_constraint_label": "保持一级平面关系", "face_push_pull_planar_relation_constraint_label": "保持一级平面关系",
"face_push_pull_planar_constraint_status": "blocked" if blockers else "ready", "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]: def _with_face_planar_relation_constraint(
plan = self.push_pull_plan(face_id, distance) self,
constraint_fields = self._face_push_pull_planar_relation_constraint_fields(face_id, plan) face_id: int,
plan.update(constraint_fields) plan: dict[str, object],
plan["resize_strategy"] = "push-pull-planar-face-keep-relations" *,
plan["edit_strategy_label"] = "保持一级平面关系的拉伸/切除" operation_label: str,
plan["edit_semantics"] = ( edit_strategy_label: str,
"沿当前 Face 法向拉伸/切除,同时要求当前 Face 与共享边一级相邻平面的平行/垂直关系" 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": 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 "") blockers = str(constraint_fields.get("face_push_pull_planar_constraint_blockers") or "")
plan["status"] = "blocked" plan["status"] = "blocked"
@@ -368,6 +380,21 @@ class OperationMixin:
plan["message"] = str(constraint_fields["face_push_pull_planar_constraint_summary"]) plan["message"] = str(constraint_fields["face_push_pull_planar_constraint_summary"])
return plan 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: def _multistep_prismatic_plan_blocker(self, face_id: int, operation_label: str) -> str:
try: try:
quick_info = self.quick_face_info(face_id) 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]: 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): if face_id < 0 or face_id >= len(self.faces):
raise ValueError(f"Unknown face id {face_id}") 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( def _rectangular_prismatic_size_resize_plan(
self, self,
*, *,
@@ -8144,6 +8208,35 @@ class OperationMixin:
f"risk={plan['risk']}. {result_check}" 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( def face_area_local_resize_preview_polydata(
self, self,
face_id: int, face_id: int,
@@ -8243,6 +8336,33 @@ class OperationMixin:
f"risk={plan['risk']}. {result_check}" 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( def face_size_owning_scale_preview_polydata(
self, self,
face_id: int, face_id: int,
@@ -8318,7 +8438,10 @@ class OperationMixin:
matched_face_id: int, matched_face_id: int,
facts: dict[str, object], facts: dict[str, object],
) -> str: ) -> 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 "" return ""
if str(plan.get("face_push_pull_planar_constraint_status") or "") != "ready": 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.") raise RuntimeError("Face planar relation check was requested but the original constraint plan is not ready.")
+65 -18
View File
@@ -1507,10 +1507,13 @@ class WindowActionMixin:
"面面积(缩放特征)", "面面积(缩放特征)",
"面内长度(局部重建)", "面内长度(局部重建)",
"面内宽度(局部重建)", "面内宽度(局部重建)",
"面内长度(保持关系)",
"面内宽度(保持关系)",
"面内长度(缩放特征)", "面内长度(缩放特征)",
"面内宽度(缩放特征)", "面内宽度(缩放特征)",
"偏移(局部重建)", "偏移(局部重建)",
"中心(局部重建)", "中心(局部重建)",
"中心(保持关系)",
"圆柱高度调整", "圆柱高度调整",
"高度(缩放特征)缩放所属对象", "高度(缩放特征)缩放所属对象",
} }
@@ -1531,8 +1534,10 @@ class WindowActionMixin:
"resize_face_area_local", "resize_face_area_local",
"resize_face_area", "resize_face_area",
"resize_face_size_local", "resize_face_size_local",
"resize_face_size_local_keep_relations",
"resize_face_size_owning_scale", "resize_face_size_owning_scale",
"move_face_center_local", "move_face_center_local",
"move_face_center_local_keep_relations",
"resize_shell_thickness", "resize_shell_thickness",
"resize_shell_thickness_owning_scale", "resize_shell_thickness_owning_scale",
"resize_cylindrical_height", "resize_cylindrical_height",
@@ -5920,10 +5925,16 @@ class WindowActionMixin:
) )
def resize_face_width_local(self) -> None: 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: 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: def resize_face_width_owning_scale(self) -> None:
self._resize_face_size_owning_scale("width") self._resize_face_size_owning_scale("width")
@@ -5931,12 +5942,13 @@ class WindowActionMixin:
def resize_face_height_owning_scale(self) -> None: def resize_face_height_owning_scale(self) -> None:
self._resize_face_size_owning_scale("height") 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: if self.model is None:
return return
axis_key = "height" if str(axis).lower() == "height" else "width" axis_key = "height" if str(axis).lower() == "height" else "width"
axis_label = "面内宽度" if axis_key == "height" else "面内长度" 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 return
if self.selected_face_id is None: if self.selected_face_id is None:
QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。") QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。")
@@ -5953,10 +5965,13 @@ class WindowActionMixin:
face_id = self.selected_face_id face_id = self.selected_face_id
plan = self._quick_blocked_face_size_local_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: if plan is None:
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) plan = self.model.face_size_local_resize_plan(face_id, target_size, axis_key)
lines = [ lines = [
f"Face: {face_id}", f"Face: {face_id}",
f"修改对象: {axis_label}(局部重建)", f"修改对象: {mode_label}",
f"当前面内长度: {_format_value(plan.get('current_face_width'))}", f"当前面内长度: {_format_value(plan.get('current_face_width'))}",
f"目标面内长度: {_format_value(plan.get('target_face_width'))}", f"目标面内长度: {_format_value(plan.get('target_face_width'))}",
f"当前面内宽度: {_format_value(plan.get('current_face_height'))}", f"当前面内宽度: {_format_value(plan.get('current_face_height'))}",
@@ -5970,21 +5985,23 @@ class WindowActionMixin:
f"影响目标: {_format_value(plan.get('local_face_deform_target_kind'))}", f"影响目标: {_format_value(plan.get('local_face_deform_target_kind'))}",
] ]
if not self._confirm_quick_edit_plan( if not self._confirm_quick_edit_plan(
f"{axis_label}(局部重建)", mode_label,
plan, plan,
lines, lines,
f"{axis_label}(局部重建)已阻止", f"{mode_label}已阻止",
f"已取消{axis_label}(局部重建)", f"已取消{mode_label}",
): ):
return return
self.clear_edit_preview(render=False) self.clear_edit_preview(render=False)
def action(): 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) return self.model.resize_face_size_local(face_id, target_size, axis_key)
self._run_edit_action( self._run_edit_action(
action, action,
operation_name=f"修改{axis_label}(局部重建)", operation_name=f"修改{mode_label}",
target=f"Face {face_id}", target=f"Face {face_id}",
parameters={ parameters={
"face_id": face_id, "face_id": face_id,
@@ -6013,6 +6030,11 @@ class WindowActionMixin:
"resize_strategy": plan.get("resize_strategy"), "resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"), "edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"), "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_status": plan.get("status"),
"resize_risk": plan.get("risk"), "resize_risk": plan.get("risk"),
"resize_message": plan.get("message"), "resize_message": plan.get("message"),
@@ -6022,7 +6044,11 @@ class WindowActionMixin:
}, },
target_kind="face", target_kind="face",
target_id=face_id, 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: def _resize_face_size_owning_scale(self, axis: str) -> None:
@@ -6361,9 +6387,16 @@ class WindowActionMixin:
) )
def move_selected_face_center_local(self) -> None: 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: if self.model is None:
return return
if self._edit_busy("请等待当前编辑完成后再执行中心(局部重建)"): mode_label = "中心(保持关系)" if keep_relations else "中心(局部重建)"
if self._edit_busy(f"请等待当前编辑完成后再执行{mode_label}"):
return return
if self.selected_face_id is None: if self.selected_face_id is None:
QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。") QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。")
@@ -6378,9 +6411,9 @@ class WindowActionMixin:
current_center = self._selected_face_center_for_translation(face_id) current_center = self._selected_face_center_for_translation(face_id)
if current_center is None: if current_center is None:
self._show_immediate_cannot_edit_message( self._show_immediate_cannot_edit_message(
"中心(局部重建)", mode_label,
"当前 Face 缺少稳定中心坐标。", "当前 Face 缺少稳定中心坐标。",
"中心(局部重建)已阻止", f"{mode_label}已阻止",
) )
return return
target_center = ( target_center = (
@@ -6390,6 +6423,9 @@ class WindowActionMixin:
) )
plan = self._quick_blocked_face_center_local_plan(face_id, target_center) plan = self._quick_blocked_face_center_local_plan(face_id, target_center)
if plan is None: if plan is None:
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) plan = self.model.face_center_local_move_plan(face_id, target_center)
lines = [ lines = [
f"Face: {face_id}", f"Face: {face_id}",
@@ -6403,21 +6439,23 @@ class WindowActionMixin:
f"影响目标: {_format_value(plan.get('local_face_deform_target_kind'))}", f"影响目标: {_format_value(plan.get('local_face_deform_target_kind'))}",
] ]
if not self._confirm_quick_edit_plan( if not self._confirm_quick_edit_plan(
"中心(局部重建)", mode_label,
plan, plan,
lines, lines,
"中心(局部重建)已阻止", f"{mode_label}已阻止",
"已取消中心(局部重建)", f"已取消{mode_label}",
): ):
return return
self.clear_edit_preview(render=False) self.clear_edit_preview(render=False)
def action(): 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) return self.model.move_face_center_local(face_id, target_center)
self._run_edit_action( self._run_edit_action(
action, action,
operation_name="中心(局部重建)", operation_name=mode_label,
target=f"Face {face_id}", target=f"Face {face_id}",
parameters={ parameters={
"face_id": face_id, "face_id": face_id,
@@ -6436,6 +6474,11 @@ class WindowActionMixin:
"resize_strategy": plan.get("resize_strategy"), "resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"), "edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"), "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_status": plan.get("status"),
"resize_risk": plan.get("risk"), "resize_risk": plan.get("risk"),
"resize_message": plan.get("message"), "resize_message": plan.get("message"),
@@ -6445,7 +6488,11 @@ class WindowActionMixin:
}, },
target_kind="face", target_kind="face",
target_id=face_id, 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: def move_selected_axis_center_by_translation(self) -> None:
+114
View File
@@ -3427,6 +3427,43 @@ class WindowStateMixin:
_triple_or_none(action_info.get("area_center")) _triple_or_none(action_info.get("area_center"))
or _triple_or_none(action_info.get("bbox_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: if is_plane:
current_face_width = _float_or_none(action_info.get("local_face_width")) current_face_width = _float_or_none(action_info.get("local_face_width"))
current_face_height = _float_or_none(action_info.get("local_face_height")) 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)}", "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": { "owning": {
"label": "缩放特征", "label": "缩放特征",
"action": "resize_face_width_owning_scale", "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)}", "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": { "owning": {
"label": "缩放特征", "label": "缩放特征",
"action": "resize_face_height_owning_scale", "action": "resize_face_height_owning_scale",
@@ -3549,6 +3636,33 @@ class WindowStateMixin:
"transform_context": {"current_center": current_face_center}, "transform_context": {"current_center": current_face_center},
**face_vector_move_limit(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": { "owning": {
"label": "移动特征", "label": "移动特征",
"action": "move_selected_face_center", "action": "move_selected_face_center",