feat: 增强一级关系识别与保持关系策略

This commit is contained in:
2026-08-10 14:56:50 +08:00
parent 12250603dd
commit 83c44265ba
24 changed files with 3460 additions and 241 deletions
+153 -23
View File
@@ -302,9 +302,11 @@ class FeatureMixin:
depth_count = 0
suppress_count = 0
existing_fillet_count = 0
existing_chamfer_count = 0
depth_limit = max(2, min(per_type_limit, limit // 12))
suppress_limit = max(2, min(per_type_limit, limit // 12))
existing_fillet_limit = max(2, min(per_type_limit, limit // 12))
existing_chamfer_limit = max(2, min(per_type_limit, limit // 12))
slot_width_limit = max(2, min(per_type_limit, limit // 10))
slot_depth_limit = max(2, min(per_type_limit, limit // 10))
slot_arc_length_limit = max(2, min(per_type_limit, limit // 10))
@@ -323,15 +325,27 @@ class FeatureMixin:
if existing_fillet_status == "blocked":
continue
support_face_ids = tuple(feature.get("feature_existing_fillet_support_face_ids", ()))
chain_face_ids = tuple(feature.get("feature_existing_fillet_chain_face_ids", ()))
chain_status = str(feature.get("existing_fillet_chain_status") or "")
is_same_radius_chain = chain_status == "same-radius-chain-candidate" and len(chain_face_ids) > 1
support_note = (
f"支撑 Face: {support_face_ids}"
if support_face_ids
else "暂未识别出稳定支撑 Face。"
)
fillet_note = (
f"这是简单等半径圆角链候选,链上 Face: {chain_face_ids};点击后会选中并预填目标半径/弧长,"
"在特征参数表修改后点击“参数化建模”,会尝试一起 defeature 后重新倒圆。"
if is_same_radius_chain
else (
"这是已有圆角/倒圆候选;点击后会选中并预填目标半径/弧长,"
"在特征参数表修改后点击“参数化建模”,会尝试 defeature 后重新倒圆。"
)
)
candidates.append(
{
"operation_key": "inspect_existing_fillet",
"operation": "修改已有圆角半径",
"operation": "修改已有圆角半径/弧长" if is_same_radius_chain else "修改已有圆角半径/弧长",
"target_kind": "face",
"target_id": item["face_id"],
"face_id": item["face_id"],
@@ -342,13 +356,9 @@ class FeatureMixin:
"current_value": feature.get("existing_fillet_radius_estimate", item["radius"]),
"current_value_label": "radius",
"status": "caution",
"risk": "medium" if len(support_face_ids) >= 2 else "high",
"risk": "high" if is_same_radius_chain or len(support_face_ids) < 2 else "medium",
"confidence": item["confidence"],
"note": (
"这是已有圆角/倒圆候选;点击后会选中并预填目标半径,"
"再点击“修改已有圆角半径”会尝试 defeature 后重新倒圆。"
f" {support_note}"
),
"note": f"{fillet_note} {support_note}",
}
)
existing_fillet_count += 1
@@ -356,7 +366,7 @@ class FeatureMixin:
if diameter_count < per_type_limit and feature_guess not in {
"round/fillet candidate",
"boss/outer-round candidate",
}:
} and str(item.get("resize_status") or "") != "blocked":
candidates.append(
{
"operation_key": "resize_cylinder",
@@ -388,6 +398,8 @@ class FeatureMixin:
and float(item.get("angular_span", 0.0)) < math.tau * 0.92
):
feature = self.feature_info(int(item["face_id"]))
if str(feature.get("slot_status") or "") != "candidate":
continue
slot_width = feature.get("slot_chord_width_estimate")
if slot_width_count < slot_width_limit and isinstance(slot_width, (int, float)) and float(slot_width) > 0:
candidates.append(
@@ -666,17 +678,70 @@ class FeatureMixin:
plane_count = 0
shell_thickness_count = 0
shell_thickness_limit = max(2, min(per_type_limit, limit // 10))
shell_candidate_rows: list[dict[str, object]] = []
shell_scan_pool_limit = max(shell_thickness_limit * 4, 12)
face_scan_limit = len(self.faces) if max_scan_faces is None else min(len(self.faces), max(0, int(max_scan_faces)))
for face_id, face in enumerate(self.faces[:face_scan_limit]):
if progress_callback is not None and face_id % 30 == 0:
progress_callback()
if plane_count >= per_type_limit and shell_thickness_count >= shell_thickness_limit:
if (
plane_count >= per_type_limit
and len(shell_candidate_rows) >= shell_scan_pool_limit
and existing_chamfer_count >= existing_chamfer_limit
):
break
surf = BRepAdaptor_Surface(face)
if surf.GetType() != GeomAbs_Plane:
continue
props = GProp_GProps()
brepgprop.SurfaceProperties(face, props)
feature: dict[str, object] | None = None
quick_info: dict[str, object] | None = None
if existing_chamfer_count < existing_chamfer_limit:
quick_info = self.quick_face_info(face_id)
width = _float_or_none(quick_info.get("local_face_width"))
height = _float_or_none(quick_info.get("local_face_height"))
aspect = max(width or 0.0, height or 0.0) / max(min(width or 0.0, height or 0.0), 1e-9)
if (
int(quick_info.get("boundary_edges", 0) or 0) == 4
and not bool(quick_info.get("has_inner_boundaries"))
and aspect >= 1.8
):
feature = self.feature_info(face_id)
if str(feature.get("existing_chamfer_status") or "") == "candidate":
support_face_ids = tuple(feature.get("feature_existing_chamfer_support_face_ids", ()))
long_edge_ids = tuple(feature.get("feature_existing_chamfer_long_edge_ids", ()))
candidates.append(
{
"operation_key": "inspect_existing_chamfer",
"operation": "修改已有倒角距离",
"target_kind": "face",
"target_id": face_id,
"face_id": face_id,
"part_id": self.face_part_ids[face_id],
"solid_id": self.face_solid_ids[face_id],
"surface": "plane",
"feature_guess": "chamfer candidate",
"current_value": feature.get("existing_chamfer_distance_estimate"),
"current_value_label": "chamfer_distance",
"status": "caution",
"risk": str(feature.get("existing_chamfer_risk") or "medium"),
"confidence": str(feature.get("recognition_confidence") or "medium"),
"note": (
"这是简单已有倒角斜面;点击后会选中该 Face,并把倒角距离填入参数表。"
f" 支撑 Face: {support_face_ids}; 长边 Edge: {long_edge_ids}"
),
}
)
existing_chamfer_count += 1
continue
if plane_count < per_type_limit or len(shell_candidate_rows) < shell_scan_pool_limit:
quick_info = quick_info or self.quick_face_info(face_id)
boundary_edges = int(quick_info.get("boundary_edges", 0) or 0)
if bool(quick_info.get("has_inner_boundaries")) or boundary_edges > 4:
feature = feature or self.feature_info(face_id)
if feature.get("multistep_prismatic_status") == "blocked":
continue
if plane_count < per_type_limit:
if detailed:
direction_info = self._plane_push_pull_direction(face_id, surf)
@@ -709,12 +774,21 @@ class FeatureMixin:
}
)
plane_count += 1
if shell_thickness_count < shell_thickness_limit:
feature = self.feature_info(face_id)
if len(shell_candidate_rows) < shell_scan_pool_limit:
feature = feature or self.feature_info(face_id)
if feature.get("shell_region_status") == "candidate":
shell_confidence = str(feature.get("shell_confidence", "low"))
shell_risk = "low" if shell_confidence == "high" else "medium" if shell_confidence == "medium" else "high"
candidates.append(
shell_note = (
"快速扫描:已找到投影重叠的相对平面;点击后会填入参考目标厚度,"
"执行时会移动当前平面区域来改变局部壳体厚度。"
)
if feature.get("open_shell_context_status") == "limited":
shell_note = (
f"{shell_note} 已识别为开口薄壁壳体上下文;"
"完整抽壳/开口面编辑暂未实现,当前只开放壳体厚度、平面推拉或整体缩放。"
)
shell_candidate_rows.append(
{
"operation_key": "resize_shell_thickness",
"operation": "调整壳体厚度",
@@ -730,13 +804,26 @@ class FeatureMixin:
"status": "ready" if shell_confidence == "high" else "caution",
"risk": shell_risk,
"confidence": shell_confidence,
"note": (
"快速扫描:已找到投影重叠的相对平面;点击后会填入参考目标厚度,"
"执行时会移动当前平面区域来改变局部壳体厚度。"
),
"note": shell_note,
"open_shell_context_status": feature.get("open_shell_context_status", ""),
}
)
shell_thickness_count += 1
def _shell_candidate_sort_key(item: dict[str, object]) -> tuple[int, int, int, float, int]:
confidence_rank = {"high": 0, "medium": 1, "low": 2}.get(str(item.get("confidence") or ""), 3)
risk_rank = {"low": 0, "medium": 1, "high": 2, "blocked": 3}.get(str(item.get("risk") or ""), 3)
thickness = _float_or_none(item.get("current_value"))
return (
0 if item.get("open_shell_context_status") == "limited" else 1,
confidence_rank,
risk_rank,
thickness if thickness is not None else float("inf"),
int(item.get("face_id", 10**9) or 10**9),
)
for shell_candidate in sorted(shell_candidate_rows, key=_shell_candidate_sort_key)[:shell_thickness_limit]:
candidates.append(shell_candidate)
shell_thickness_count += 1
fillet_edge_count = 0
chamfer_edge_count = 0
@@ -959,6 +1046,32 @@ class FeatureMixin:
candidate.update(self._cylinder_end_opening_info(face_id, surf))
candidate.update(_cylinder_resize_readiness(candidate))
candidate.update(_cylinder_boss_resize_readiness(candidate))
try:
feature = self.feature_info(face_id)
for key in (
"feature_guess",
"feature_type",
"feature_edit_actions",
"confidence",
"slot_kind",
"slot_status",
"slot_risk",
"slot_blockers",
"resize_status",
"resize_risk",
"resize_blockers",
"resize_note",
"boss_resize_status",
"boss_resize_risk",
"boss_resize_blockers",
"boss_resize_note",
"recognition_risk",
"recognition_blockers",
):
if feature.get(key) not in {None, ""}:
candidate[key] = feature.get(key)
except Exception:
pass
candidates.append(candidate)
if len(candidates) >= limit:
break
@@ -976,6 +1089,24 @@ class FeatureMixin:
}
current_diameter = float(info["diameter"])
feature = self.feature_info(face_id)
if str(feature.get("slot_status") or "") == "blocked":
blocker = str(feature.get("slot_blockers") or "当前槽/半孔属于复杂槽或多槽组,暂不开放稳定修改。")
return {
"status": "blocked",
"risk": "blocked",
"message": blocker,
"warnings": "",
"blockers": blocker,
"face_id": face_id,
"part_id": info.get("part_id"),
"solid_id": info.get("solid_id"),
"current_diameter": current_diameter,
"target_diameter": new_diameter,
"feature_type": feature.get("feature_type"),
"feature_guess": feature.get("feature_guess"),
"slot_status": feature.get("slot_status"),
"slot_blockers": blocker,
}
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
axis_range = self._cylindrical_axis_range(
face_id,
@@ -1271,8 +1402,7 @@ class FeatureMixin:
if angular_span is None or angular_span <= 1e-6 or angular_span >= math.tau * 0.92:
blockers.append("Selected slot does not have a stable partial-cylinder angular span.")
if slot_status and slot_status != "candidate":
risk = _max_risk(risk, "high")
warnings.append(f"Slot candidate status is {slot_status}.")
blockers.append(str(feature.get("slot_blockers") or info.get("slot_blockers") or f"Slot candidate status is {slot_status}."))
span = min(max(float(angular_span or 0.0), 1e-6), math.tau - 1e-6)
sin_half_span = math.sin(span / 2.0)
@@ -1464,8 +1594,7 @@ class FeatureMixin:
if target_angular_span >= math.tau * 0.92:
blockers.append("Target slot angular span must remain below a near-full cylinder.")
if slot_status and slot_status != "candidate":
risk = _max_risk(risk, "high")
warnings.append(f"Slot candidate status is {slot_status}.")
blockers.append(str(feature.get("slot_blockers") or info.get("slot_blockers") or f"Slot candidate status is {slot_status}."))
delta_span = None if current_span <= 0 else target_angular_span - current_span
delta_ratio = (
@@ -1676,8 +1805,7 @@ class FeatureMixin:
risk = _max_risk(risk, "medium")
warnings.append("Slot recognition confidence is low.")
if slot_status and slot_status != "candidate":
risk = _max_risk(risk, "high")
warnings.append(f"Slot candidate status is {slot_status}.")
blockers.append(str(feature.get("slot_blockers") or info.get("slot_blockers") or f"Slot candidate status is {slot_status}."))
cutter_plan: dict[str, object] = {}
fill_plan: dict[str, object] = {}
@@ -2053,6 +2181,8 @@ class FeatureMixin:
blockers.append("Current slot face is not recognized as one end of a paired obround slot.")
else:
blockers.append("Manual paired Face ID could not be used as a compatible obround slot end.")
if str(feature.get("slot_status") or "") not in {"", "candidate"}:
blockers.append(str(feature.get("slot_blockers") or f"Slot candidate status is {feature.get('slot_status')}."))
if target_total_length <= 0:
blockers.append("Target slot total length must be greater than 0.")
+2
View File
@@ -117,6 +117,8 @@ def _execute(model: StepModel, operation: str, args: list[object]) -> str:
return model.resize_ellipse_edge_axis_radius(int(args[0]), float(args[1]), axis_kind=axis_kind)
if operation == "resize_existing_fillet":
return model.resize_existing_fillet(int(args[0]), float(args[1]))
if operation == "resize_existing_chamfer":
return model.resize_existing_chamfer(int(args[0]), float(args[1]))
if operation == "fillet_edge":
return model.fillet_edge(int(args[0]), float(args[1]))
if operation == "chamfer_edge":
+1056 -52
View File
File diff suppressed because it is too large Load Diff
+626 -63
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -17,6 +17,7 @@ USER_OPERATION_PRIORITY: dict[str, int] = {
"resize_boss_height": 41,
"move_boss_axis": 44,
"inspect_existing_fillet": 50,
"inspect_existing_chamfer": 51,
"fillet_edge": 52,
"chamfer_edge": 53,
"resize_shell_thickness": 60,
@@ -77,6 +78,9 @@ def feature_recognition_priority(info: Mapping[str, object]) -> int:
combined_text = "".join(item for item in (feature_type, feature_actions, ready_actions) if item)
if _text(info.get("existing_chamfer_status")) == "candidate" or feature_guess == "chamfer candidate":
return 50
if surface == "plane":
return 10
+116
View File
@@ -79,6 +79,29 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
"first_level_fact_ignored_relation_depths",
"first_level_fact_ignored_relation_note",
"first_level_fact_summary",
"first_level_planar_relation_status",
"first_level_planar_relation_scope",
"first_level_planar_relation_count",
"first_level_planar_relation_parallel_count",
"first_level_planar_relation_perpendicular_count",
"first_level_planar_relation_angled_count",
"first_level_planar_relation_face_face_count",
"first_level_planar_relation_edge_axis_count",
"first_level_planar_relation_rows",
"first_level_planar_relation_observed_modes",
"first_level_planar_relation_summary",
"first_level_planar_relation_note",
"edge_length_planar_relation_constraint_requested",
"edge_length_planar_relation_constraint_label",
"edge_length_planar_constraint_status",
"edge_length_planar_constraint_summary",
"edge_length_planar_constraint_side_face_count",
"edge_length_planar_constraint_parallel_count",
"edge_length_planar_constraint_perpendicular_count",
"edge_length_planar_constraint_angled_count",
"edge_length_planar_constraint_observed_modes",
"edge_length_planar_constraint_blockers",
"edge_length_planar_constraint_warnings",
"selected_edge_ids",
"selected_edge_count",
"first_level_vertex_points",
@@ -146,6 +169,11 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
"existing_fillet_radius_estimate",
"existing_fillet_angular_span",
"existing_fillet_arc_length_estimate",
"existing_chamfer_distance_estimate",
"existing_chamfer_cross_edge_length_estimate",
"existing_chamfer_long_edge_length_estimate",
"existing_chamfer_support_angle_degrees",
"existing_chamfer_face_angle_degrees",
"current_depth",
"target_depth",
"delta_depth",
@@ -219,6 +247,25 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
"shell_movement_alignment",
"shell_note",
"shell_region_note",
"open_shell_context_status",
"open_shell_kind",
"open_shell_axis",
"open_shell_side",
"open_shell_open_direction",
"open_shell_wall_thickness_estimate",
"open_shell_open_side_area_ratio",
"open_shell_closed_side_area_ratio",
"open_shell_current_face_role",
"open_shell_current_face_id",
"open_shell_rim_face_ids",
"open_shell_closed_side_face_ids",
"open_shell_inner_face_ids",
"open_shell_thin_wall_face_ids",
"open_shell_related_face_ids",
"open_shell_highlight_face_ids",
"open_shell_limited_action",
"open_shell_blockers",
"open_shell_note",
],
),
(
@@ -293,6 +340,17 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
"existing_fillet_kind",
"existing_fillet_status",
"existing_fillet_note",
"feature_existing_chamfer_face_ids",
"feature_existing_chamfer_support_face_ids",
"feature_existing_chamfer_end_face_ids",
"feature_existing_chamfer_short_edge_ids",
"feature_existing_chamfer_long_edge_ids",
"existing_chamfer_kind",
"existing_chamfer_status",
"existing_chamfer_risk",
"existing_chamfer_warnings",
"existing_chamfer_blockers",
"existing_chamfer_note",
"feature_edit_actions",
"resize_status",
"resize_strategy",
@@ -496,6 +554,29 @@ INFO_LABELS = {
"first_level_fact_ignored_relation_depths": "暂不传播层级",
"first_level_fact_ignored_relation_note": "暂不传播说明",
"first_level_fact_summary": "一级事实摘要",
"first_level_planar_relation_status": "一级平面关系状态",
"first_level_planar_relation_scope": "一级平面关系范围",
"first_level_planar_relation_count": "一级平面关系数",
"first_level_planar_relation_parallel_count": "一级平行关系数",
"first_level_planar_relation_perpendicular_count": "一级垂直关系数",
"first_level_planar_relation_angled_count": "一级斜交关系数",
"first_level_planar_relation_face_face_count": "Face-Face 平面关系数",
"first_level_planar_relation_edge_axis_count": "Face-Edge 方向关系数",
"first_level_planar_relation_rows": "一级平面关系列表",
"first_level_planar_relation_observed_modes": "已识别平面关系",
"first_level_planar_relation_summary": "一级平面关系摘要",
"first_level_planar_relation_note": "一级平面关系说明",
"edge_length_planar_relation_constraint_requested": "已请求保持一级平面关系",
"edge_length_planar_relation_constraint_label": "一级平面关系约束",
"edge_length_planar_constraint_status": "端面推拉约束状态",
"edge_length_planar_constraint_summary": "端面推拉约束摘要",
"edge_length_planar_constraint_side_face_count": "端面推拉侧面数",
"edge_length_planar_constraint_parallel_count": "端面推拉平行关系数",
"edge_length_planar_constraint_perpendicular_count": "端面推拉垂直关系数",
"edge_length_planar_constraint_angled_count": "端面推拉斜交关系数",
"edge_length_planar_constraint_observed_modes": "端面推拉已识别关系",
"edge_length_planar_constraint_blockers": "端面推拉阻止原因",
"edge_length_planar_constraint_warnings": "端面推拉警告",
"selected_edge_ids": "当前 Edge",
"selected_edge_count": "当前 Edge 数",
"first_level_vertex_points": "一级端点 Vertex",
@@ -671,6 +752,25 @@ INFO_LABELS = {
"shell_movement_alignment": "壳体移动方向匹配度",
"shell_note": "壳体识别说明",
"shell_region_note": "壳体识别说明",
"open_shell_context_status": "开口壳体上下文状态",
"open_shell_kind": "开口壳体类型",
"open_shell_axis": "开口方向轴",
"open_shell_side": "开口侧",
"open_shell_open_direction": "开口方向",
"open_shell_wall_thickness_estimate": "开口壳体壁厚估算",
"open_shell_open_side_area_ratio": "开口侧面积占比",
"open_shell_closed_side_area_ratio": "封闭侧面积占比",
"open_shell_current_face_role": "当前 Face 壳体角色",
"open_shell_current_face_id": "当前开口壳体 Face",
"open_shell_rim_face_ids": "开口边界 Face",
"open_shell_closed_side_face_ids": "封闭侧 Face",
"open_shell_inner_face_ids": "内壁/内底 Face",
"open_shell_thin_wall_face_ids": "薄壁 Face",
"open_shell_related_face_ids": "开口壳体相关 Face",
"open_shell_highlight_face_ids": "开口壳体高亮 Face",
"open_shell_limited_action": "受限壳体操作",
"open_shell_blockers": "开口壳体限制原因",
"open_shell_note": "开口壳体说明",
"u_range": "U 参数范围",
"v_range": "V 参数范围",
"first_parameter": "起始参数",
@@ -723,6 +823,22 @@ INFO_LABELS = {
"existing_fillet_kind": "已有圆角类型",
"existing_fillet_status": "已有圆角识别状态",
"existing_fillet_radius_estimate": "已有圆角半径估算",
"feature_existing_chamfer_face_ids": "已有倒角 Face",
"feature_existing_chamfer_support_face_ids": "已有倒角支撑 Face",
"feature_existing_chamfer_end_face_ids": "已有倒角端面 Face",
"feature_existing_chamfer_short_edge_ids": "已有倒角短边 Edge",
"feature_existing_chamfer_long_edge_ids": "已有倒角长边 Edge",
"existing_chamfer_kind": "已有倒角类型",
"existing_chamfer_status": "已有倒角识别状态",
"existing_chamfer_risk": "已有倒角风险",
"existing_chamfer_warnings": "已有倒角警告",
"existing_chamfer_blockers": "已有倒角阻止原因",
"existing_chamfer_distance_estimate": "已有倒角距离估算",
"existing_chamfer_cross_edge_length_estimate": "已有倒角截面边长估算",
"existing_chamfer_long_edge_length_estimate": "已有倒角长度估算",
"existing_chamfer_support_angle_degrees": "已有倒角支撑面夹角",
"existing_chamfer_face_angle_degrees": "已有倒角面角度",
"existing_chamfer_note": "已有倒角识别说明",
"feature_reference_radius": "特征参考半径",
"feature_reference_diameter": "特征参考直径",
"feature_cone_small_radius": "锥孔小端半径",
+155 -20
View File
@@ -1487,6 +1487,7 @@ class WindowActionMixin:
"move_circular_edge_axis_center",
"resize_ellipse_edge_axis_radius",
"resize_existing_fillet",
"resize_existing_chamfer",
"fillet_edge",
"chamfer_edge",
"chamfer_edge_asymmetric",
@@ -4101,6 +4102,92 @@ class WindowActionMixin:
isolation=isolation,
)
def resize_existing_chamfer(self) -> None:
if self.model is None:
return
if self._edit_busy("请等待当前编辑完成后再修改已有倒角。"):
return
if self.selected_face_id is None:
QMessageBox.information(self, "未选择已有倒角", "请先选择一个已有倒角斜面 Face。")
return
try:
target_distance = float(self.edge_chamfer_distance_input.text())
except ValueError:
QMessageBox.critical(self, "距离无效", "请输入数字形式的目标倒角距离。")
return
face_id = self.selected_face_id
plan = self.model.existing_chamfer_resize_plan(face_id, target_distance)
isolation = self._isolation_for_plan(plan, "resize_existing_chamfer", [face_id, target_distance])
if plan["status"] == "blocked":
self._show_blocked_plan_message("修改已有倒角", plan, "已有倒角距离修改已阻止")
return
warnings = str(plan.get("warnings", ""))
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
result = QMessageBox.question(
self,
"确认修改已有倒角距离",
(
f"Face: {plan['face_id']}\n"
f"当前估算距离: {_format_value(plan['current_distance'])}\n"
f"目标距离: {_format_value(plan['target_distance'])}\n"
f"距离变化量: {_format_value(plan['delta_distance'])}\n"
f"距离变化比例: {_format_percent(plan['distance_delta_ratio'])}\n"
f"支撑 Face: {_format_value(plan.get('feature_existing_chamfer_support_face_ids', ''))}\n"
f"长边 Edge: {_format_value(plan.get('feature_existing_chamfer_long_edge_ids', ''))}\n"
f"策略: {plan['resize_strategy']}\n"
f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n"
f"建模意图: {_format_value(plan.get('edit_semantics'))}\n"
f"风险: {plan['risk']}\n\n"
f"{warnings_line}"
f"{plan['message']}\n\n"
"继续操作会先尝试移除当前倒角斜面,再对恢复出的锐边重新倒角。\n"
"这不是 CAD 历史特征参数编辑,复杂倒角可能失败。\n\n"
"确定继续吗?"
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if result != QMessageBox.StandardButton.Yes:
self.statusBar().showMessage("已取消修改已有倒角距离")
return
def action():
return self.model.resize_existing_chamfer(face_id, target_distance)
self._run_edit_action(
action,
operation_name="修改已有倒角距离",
target=f"Face {face_id}",
parameters={
"part_id": plan.get("part_id"),
"solid_id": plan.get("solid_id"),
"old_distance": plan.get("current_distance"),
"new_distance": target_distance,
"delta_distance": plan.get("delta_distance"),
"distance_delta_ratio": plan.get("distance_delta_ratio"),
"feature_type": plan.get("feature_type"),
"feature_guess": plan.get("feature_guess"),
"confidence": plan.get("confidence"),
"existing_chamfer_status": plan.get("status"),
"existing_chamfer_risk": plan.get("risk"),
"existing_chamfer_message": plan.get("message"),
"existing_chamfer_warnings": plan.get("warnings"),
"existing_chamfer_blockers": plan.get("blockers"),
"feature_existing_chamfer_support_face_ids": plan.get("feature_existing_chamfer_support_face_ids"),
"feature_existing_chamfer_long_edge_ids": plan.get("feature_existing_chamfer_long_edge_ids"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"resize_note": plan.get("resize_note"),
"ui_preview": "skipped-for-existing-chamfer",
},
target_kind="face",
target_id=face_id,
isolation=isolation,
)
def fillet_edge(self) -> None:
if self.model is None:
return
@@ -4483,6 +4570,7 @@ class WindowActionMixin:
f"建模意图: {_format_value(plan.get('edge_length_strategy_label', ''))}\n"
f"固定约束: {_format_value(plan.get('edge_length_constraint_summary', ''))}\n"
f"执行影响: {_format_value(plan.get('edge_length_impact_summary', ''))}\n"
f"平面关系约束: {_format_value(plan.get('edge_length_planar_constraint_summary', ''))}\n"
f"策略: {_format_value(plan.get('resize_strategy', ''))}\n"
f"局部形变目标: {_format_value(plan.get('local_edge_deform_target_kind', ''))}\n"
f"局部形变: {_format_value(plan.get('local_edge_deform_anchor', ''))}\n"
@@ -4539,6 +4627,26 @@ class WindowActionMixin:
"edge_length_strategy_label": plan.get("edge_length_strategy_label"),
"edge_length_constraint_summary": plan.get("edge_length_constraint_summary"),
"edge_length_impact_summary": plan.get("edge_length_impact_summary"),
"edge_length_planar_relation_constraint_requested": plan.get(
"edge_length_planar_relation_constraint_requested"
),
"edge_length_planar_relation_constraint_label": plan.get(
"edge_length_planar_relation_constraint_label"
),
"edge_length_planar_constraint_status": plan.get("edge_length_planar_constraint_status"),
"edge_length_planar_constraint_summary": plan.get("edge_length_planar_constraint_summary"),
"edge_length_planar_constraint_side_face_count": plan.get(
"edge_length_planar_constraint_side_face_count"
),
"edge_length_planar_constraint_parallel_count": plan.get(
"edge_length_planar_constraint_parallel_count"
),
"edge_length_planar_constraint_perpendicular_count": plan.get(
"edge_length_planar_constraint_perpendicular_count"
),
"edge_length_planar_constraint_angled_count": plan.get("edge_length_planar_constraint_angled_count"),
"edge_length_planar_constraint_blockers": plan.get("edge_length_planar_constraint_blockers"),
"edge_length_planar_constraint_warnings": plan.get("edge_length_planar_constraint_warnings"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"local_edge_deform_target_kind": plan.get("local_edge_deform_target_kind"),
@@ -4650,6 +4758,7 @@ class WindowActionMixin:
f"建模意图: {_format_value(plan.get('edge_length_strategy_label', ''))}\n"
f"固定约束: {_format_value(plan.get('edge_length_constraint_summary', ''))}\n"
f"执行影响: {_format_value(plan.get('edge_length_impact_summary', ''))}\n"
f"平面关系约束: {_format_value(plan.get('edge_length_planar_constraint_summary', ''))}\n"
f"策略: {_format_value(strategy)}\n"
f"局部形变目标: {_format_value(plan.get('local_edge_deform_target_kind', ''))}\n"
f"局部形变: {_format_value(plan.get('local_edge_deform_anchor', ''))}\n"
@@ -4707,6 +4816,26 @@ class WindowActionMixin:
"edge_length_strategy_label": plan.get("edge_length_strategy_label"),
"edge_length_constraint_summary": plan.get("edge_length_constraint_summary"),
"edge_length_impact_summary": plan.get("edge_length_impact_summary"),
"edge_length_planar_relation_constraint_requested": plan.get(
"edge_length_planar_relation_constraint_requested"
),
"edge_length_planar_relation_constraint_label": plan.get(
"edge_length_planar_relation_constraint_label"
),
"edge_length_planar_constraint_status": plan.get("edge_length_planar_constraint_status"),
"edge_length_planar_constraint_summary": plan.get("edge_length_planar_constraint_summary"),
"edge_length_planar_constraint_side_face_count": plan.get(
"edge_length_planar_constraint_side_face_count"
),
"edge_length_planar_constraint_parallel_count": plan.get(
"edge_length_planar_constraint_parallel_count"
),
"edge_length_planar_constraint_perpendicular_count": plan.get(
"edge_length_planar_constraint_perpendicular_count"
),
"edge_length_planar_constraint_angled_count": plan.get("edge_length_planar_constraint_angled_count"),
"edge_length_planar_constraint_blockers": plan.get("edge_length_planar_constraint_blockers"),
"edge_length_planar_constraint_warnings": plan.get("edge_length_planar_constraint_warnings"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"local_edge_deform_target_kind": plan.get("local_edge_deform_target_kind"),
@@ -4894,7 +5023,7 @@ class WindowActionMixin:
float(getattr(self, f"{prefix}_z_input").text()),
)
except (AttributeError, ValueError):
QMessageBox.critical(self, "Edge endpoint coordinate invalid", "Please enter numeric X, Y and Z values.")
QMessageBox.critical(self, "Edge端点坐标无效", "请输入数字形式的 X、Y、Z 目标坐标。")
return None
def move_edge_start_point(self) -> None:
@@ -4992,10 +5121,10 @@ class WindowActionMixin:
def move_edge_center_point(self) -> None:
if self.model is None:
return
if self._edit_busy("Please wait for the current edit to finish before moving an Edge center."):
if self._edit_busy("请等待当前编辑完成后再移动 Edge 中心。"):
return
if self.selected_edge_id is None:
QMessageBox.information(self, "No Edge selected", "Please select an Edge first.")
QMessageBox.information(self, "未选择Edge", "请先选择一条 Edge。")
return
try:
target_center = (
@@ -5004,7 +5133,7 @@ class WindowActionMixin:
float(self.edge_center_z_input.text()),
)
except (AttributeError, ValueError):
QMessageBox.critical(self, "Edge center coordinate invalid", "Please enter numeric X, Y and Z values.")
QMessageBox.critical(self, "Edge中心坐标无效", "请输入数字形式的 X、Y、Z 目标坐标。")
return
edge_id = self.selected_edge_id
@@ -5012,12 +5141,13 @@ class WindowActionMixin:
isolation = self._isolation_for_plan(plan, "move_edge_center", [edge_id, target_center])
lines = [
f"Edge: {edge_id}",
f"Current center: {_format_value(plan.get('current_edge_center'))}",
f"Target center: {_format_value(plan.get('target_edge_center'))}",
f"Move vector: {_format_value(plan.get('moved_edge_center_delta'))}",
f"Edge length: {_format_value(plan.get('current_length'))}",
f"Strategy: {_format_value(plan.get('resize_strategy'))}",
f"Rebuilt faces: {_format_value(plan.get('local_edge_deform_face_count'))}",
f"当前中心: {_format_value(plan.get('current_edge_center'))}",
f"目标中心: {_format_value(plan.get('target_edge_center'))}",
f"移动量: {_format_value(plan.get('moved_edge_center_delta'))}",
f"Edge长度: {_format_value(plan.get('current_length'))}",
f"建模语义: {_format_value(plan.get('edit_strategy_label'))}",
f"影响范围: {_format_value(plan.get('edit_semantics'))}",
f"重建Face数: {_format_value(plan.get('local_edge_deform_face_count'))}",
]
if not self._confirm_quick_edit_plan(
"移动Edge中心",
@@ -5054,6 +5184,8 @@ class WindowActionMixin:
"local_edge_deform_start_move": plan.get("local_edge_deform_start_move"),
"local_edge_deform_end_move": plan.get("local_edge_deform_end_move"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"resize_status": plan.get("status"),
"resize_risk": plan.get("risk"),
"resize_message": plan.get("message"),
@@ -5068,10 +5200,10 @@ class WindowActionMixin:
def _move_edge_endpoint(self, endpoint_role: str) -> None:
if self.model is None:
return
if self._edit_busy("Please wait for the current edit to finish before moving an Edge endpoint."):
if self._edit_busy("请等待当前编辑完成后再移动 Edge 端点。"):
return
if self.selected_edge_id is None:
QMessageBox.information(self, "No Edge selected", "Please select an Edge first.")
QMessageBox.information(self, "未选择Edge", "请先选择一条 Edge。")
return
target_point = self._edge_endpoint_target_from_inputs(endpoint_role)
@@ -5084,14 +5216,15 @@ class WindowActionMixin:
title = "移动Edge起点" if endpoint_role == "start" else "移动Edge终点"
lines = [
f"Edge: {edge_id}",
f"Endpoint: {_format_value(plan.get('edge_endpoint_label'))}",
f"Current endpoint: {_format_value(plan.get('current_endpoint_point'))}",
f"Target endpoint: {_format_value(plan.get('target_endpoint_point'))}",
f"Move vector: {_format_value(plan.get('moved_endpoint_delta'))}",
f"Current Edge length: {_format_value(plan.get('current_length'))}",
f"Result Edge length: {_format_value(plan.get('target_length'))}",
f"Strategy: {_format_value(plan.get('resize_strategy'))}",
f"Rebuilt faces: {_format_value(plan.get('local_edge_deform_face_count'))}",
f"端点: {_format_value(plan.get('edge_endpoint_label'))}",
f"当前端点: {_format_value(plan.get('current_endpoint_point'))}",
f"目标端点: {_format_value(plan.get('target_endpoint_point'))}",
f"移动量: {_format_value(plan.get('moved_endpoint_delta'))}",
f"当前Edge长度: {_format_value(plan.get('current_length'))}",
f"结果Edge长度: {_format_value(plan.get('target_length'))}",
f"建模语义: {_format_value(plan.get('edit_strategy_label'))}",
f"影响范围: {_format_value(plan.get('edit_semantics'))}",
f"重建Face数: {_format_value(plan.get('local_edge_deform_face_count'))}",
]
if not self._confirm_quick_edit_plan(
title,
@@ -5133,6 +5266,8 @@ class WindowActionMixin:
"local_edge_deform_start_move": plan.get("local_edge_deform_start_move"),
"local_edge_deform_end_move": plan.get("local_edge_deform_end_move"),
"resize_strategy": plan.get("resize_strategy"),
"edit_strategy_label": plan.get("edit_strategy_label"),
"edit_semantics": plan.get("edit_semantics"),
"resize_status": plan.get("status"),
"resize_risk": plan.get("risk"),
"resize_message": plan.get("message"),
+4
View File
@@ -2358,6 +2358,10 @@ class WindowCoreMixin:
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
self.statusBar().showMessage(f"已选择已有圆角/倒圆候选Face {target_id}")
elif action == "inspect_existing_chamfer":
self._set_selection_mode("Feature")
self.select_feature(int(target_id))
self.statusBar().showMessage(f"已选择已有倒角候选Face {target_id}")
elif action == "fillet_edge":
self._set_selection_mode("Edge")
self.select_edge(int(target_id))
+96 -7
View File
@@ -156,6 +156,10 @@ def _feature_dimension_keys(action_info: dict[str, object]) -> tuple[str, ...]:
angular_span = _float_or_none(action_info.get("angular_span"))
if surface == "plane":
if action_info.get("multistep_prismatic_status") == "blocked":
return ()
if action_info.get("existing_chamfer_status") == "candidate":
return ("existing_chamfer_distance_estimate",)
if action_info.get("prismatic_profile_status") == "candidate":
keys = ["local_face_width", "local_face_height"]
if action_info.get("prismatic_extrusion_status") == "candidate":
@@ -195,6 +199,8 @@ def _feature_dimension_keys(action_info: dict[str, object]) -> tuple[str, ...]:
if feature_guess == "boss/outer-round candidate":
return ("boss_diameter", "boss_height")
if feature_guess == "round/fillet candidate":
if str(action_info.get("existing_fillet_status") or "") == "blocked":
return ()
return ("existing_fillet_radius_estimate", "existing_fillet_arc_length_estimate")
return ("generic_cylinder_diameter", "cylinder_height")
@@ -1029,13 +1035,23 @@ class WindowStateMixin:
is_generic_surface = has_face and surface in FREEFORM_FACE_SURFACES
is_hole_or_groove = is_cylinder and feature_guess == "hole/groove candidate"
is_boss = is_cylinder and feature_guess == "boss/outer-round candidate"
is_existing_fillet = is_cylinder and feature_guess == "round/fillet candidate"
is_existing_fillet = (
is_cylinder
and feature_guess == "round/fillet candidate"
and str(action_info.get("existing_fillet_status") or "") != "blocked"
)
is_existing_chamfer = (
is_plane
and feature_guess == "chamfer candidate"
and str(action_info.get("existing_chamfer_status") or "") == "candidate"
)
is_full_cylinder = _is_effectively_full_cylinder(action_info)
is_slot_or_half_hole = (
is_hole_or_groove
and not is_full_cylinder
and angular_span is not None
and angular_span < math.tau * 0.92
and str(action_info.get("slot_status") or "") == "candidate"
and _float_or_none(action_info.get("slot_chord_width_estimate")) is not None
)
is_blind = action_info.get("cylinder_end_type") == "blind"
@@ -1049,8 +1065,8 @@ class WindowStateMixin:
)
has_fillet_support = len(_int_values(action_info.get("feature_existing_fillet_support_face_ids"))) >= 2
is_line_edge = has_edge and curve == "line"
can_push = has_model and is_plane
can_resize_shell = has_model and is_shell_candidate
can_push = has_model and is_plane and not is_existing_chamfer
can_resize_shell = has_model and is_shell_candidate and not is_existing_chamfer
can_resize_hole = has_model and is_hole_or_groove
can_resize_slot = has_model and is_slot_or_half_hole
can_resize_boss = has_model and is_boss and is_full_cylinder
@@ -1535,7 +1551,7 @@ class WindowStateMixin:
"Face:面内长度/宽度、中心、偏移、壳体厚度。\n"
"孔/槽:孔径、轴心、封堵、盲孔/盲槽深度、槽宽/槽深/弧长/总长。\n"
"凸台:圆柱凸台直径/高度/轴心;矩形凸台/矩形槽口袋长宽、中心、高度/深度;多台阶矩形凸台顶层规则台阶。\n"
"圆角/倒角:简单已有圆角半径/弧长,直线 Edge 新增圆角/倒角。\n"
"圆角/倒角:简单已有圆角半径/弧长、简单等半径圆角链半径/弧长、已有等距倒角距离,直线 Edge 新增圆角/倒角。\n"
"Edge/解析曲面:直线 Edge 长度/端点/中心、圆/椭圆 Edge、简单圆锥/球/环面。\n"
"受限:复杂链式特征、二级/三级拓扑传播和原 CAD 历史恢复。"
)
@@ -2566,7 +2582,16 @@ class WindowStateMixin:
is_generic_surface = has_face and surface in FREEFORM_FACE_SURFACES
is_hole_or_groove = is_cylinder and feature_guess == "hole/groove candidate"
is_boss = is_cylinder and feature_guess == "boss/outer-round candidate"
is_existing_fillet = is_cylinder and feature_guess == "round/fillet candidate"
is_existing_fillet = (
is_cylinder
and feature_guess == "round/fillet candidate"
and str(action_info.get("existing_fillet_status") or "") != "blocked"
)
is_existing_chamfer = (
is_plane
and feature_guess == "chamfer candidate"
and str(action_info.get("existing_chamfer_status") or "") == "candidate"
)
recognition_fields_present = any(
key in action_info
for key in (
@@ -2605,6 +2630,7 @@ class WindowStateMixin:
and not is_full_cylinder
and angular_span is not None
and angular_span < math.tau * 0.92
and str(action_info.get("slot_status") or "") == "candidate"
and _float_or_none(action_info.get("slot_chord_width_estimate")) is not None
)
is_blind = action_info.get("cylinder_end_type") == "blind"
@@ -2613,7 +2639,7 @@ class WindowStateMixin:
_int_values(action_info.get("feature_start_end_face_ids"))
or _int_values(action_info.get("feature_end_end_face_ids"))
)
show_generic_face_edit_specs = bool(has_face and (is_plane or is_shell_candidate))
show_generic_face_edit_specs = bool(has_face and (is_plane or is_shell_candidate) and not is_existing_chamfer)
local_face_deform_ready = bool(action_info.get("local_face_deform_ready", True))
local_face_deform_blocker = str(action_info.get("local_face_deform_blocker") or "").strip()
has_fillet_support = len(_int_values(action_info.get("feature_existing_fillet_support_face_ids"))) >= 2
@@ -2978,6 +3004,11 @@ class WindowStateMixin:
"工程特征 / 倒圆角:移除已有圆角后按目标半径重新倒圆。",
"STEP 没有原始倒圆角历史时,只能基于当前圆角面和支撑面推断;复杂 blend 会阻止或回滚。",
)
if is_existing_chamfer:
return (
"工程特征 / 倒角:移除已有倒角斜面后按目标距离重新倒角。",
"STEP 没有原始倒角历史时,只能基于当前斜面、支撑面和恢复出的锐边推断;倒角链或不等距倒角先保持受限。",
)
if is_shell_candidate:
return (
"工程特征 / 抽壳:按壳体厚度或相对面距离调整薄壁区域。",
@@ -3062,6 +3093,11 @@ class WindowStateMixin:
"优先改圆角半径;复杂圆角链先不要用整体缩放替代。",
"已有圆角需要先恢复支撑锐边再重新倒圆,支撑面不明确时失败回滚是正常保护。",
)
if is_existing_chamfer:
return (
"优先改倒角距离;复杂倒角链先保持只读或重新选 Edge 新增倒角。",
"已有倒角需要先移除斜面、恢复原始锐边再重新倒角;如果支撑面或长边不明确,会在计划阶段阻止。",
)
if is_shell_candidate:
return (
"优先改壳体厚度;普通位置变化再用偏移。",
@@ -3243,6 +3279,16 @@ class WindowStateMixin:
"再寻找可重新倒圆的锐边。复杂 blend 或支撑面不稳定时会阻止或回滚。"
),
)
elif is_existing_chamfer:
add_readonly_spec(
key="existing_chamfer_edit_semantics",
label="建模意图",
text="倒角:移除已有倒角斜面,再尝试按目标距离重新倒角。",
tip=(
"已有倒角修改不是恢复 CAD 历史参数;当前会先 defeature 当前倒角斜面,"
"再寻找恢复出的原始锐边并按目标距离重新倒角。复杂倒角链、不等距倒角或支撑面不稳定时会阻止或回滚。"
),
)
elif is_cone or is_sphere or is_torus:
add_readonly_spec(
key="analytic_surface_edit_semantics",
@@ -3502,7 +3548,7 @@ class WindowStateMixin:
used=("area_center", "bbox_center"),
)
if is_plane:
if is_plane and not is_existing_chamfer:
plane_origin = _triple_or_none(action_info.get("plane_origin"))
plane_direction = (
_triple_or_none(action_info.get("push_pull_outward_direction"))
@@ -4547,6 +4593,35 @@ class WindowStateMixin:
used=("existing_fillet_arc_length_estimate", "existing_fillet_angular_span", "angular_span"),
**positive_minimum(),
)
if is_existing_chamfer:
current_distance = _float_or_none(action_info.get("existing_chamfer_distance_estimate"))
chamfer_hint = relative_range_hint(current_distance, 0.35, 1.0)
cross_length = _float_or_none(action_info.get("existing_chamfer_cross_edge_length_estimate"))
if cross_length is not None and cross_length > 0:
chamfer_hint = (
f"{chamfer_hint} 当前倒角截面边长约 {_format_float(cross_length)}"
"目标距离过大时恢复锐边再倒角更容易失败。"
)
add_spec(
key="existing_chamfer_distance_estimate",
label="倒角距离",
current_raw=current_distance if current_distance is not None else "",
target_text=numeric_text(current_distance),
action="resize_existing_chamfer",
target_attr="edge_chamfer_distance_input",
enabled=bool(current_distance is not None and current_distance > 0),
enabled_tip="输入已有倒角面的目标距离;程序会先确认支撑面,再尝试移除旧倒角后重新倒角。",
disabled_tip="当前倒角候选缺少稳定距离估算,暂不放行修改。",
value_type="positive",
range_hint=chamfer_hint,
used=(
"existing_chamfer_distance_estimate",
"existing_chamfer_cross_edge_length_estimate",
"feature_existing_chamfer_support_face_ids",
"feature_existing_chamfer_long_edge_ids",
),
**positive_minimum(),
)
def analytic_surface_enabled(value_present: bool, capability_supported: bool = True) -> bool:
return bool(value_present and capability_supported and analytic_surface_recognition_ready)
@@ -4773,6 +4848,7 @@ class WindowStateMixin:
"auto": "自动选择:按局部边形变、端面移动、相邻圆柱编辑、缩放所属对象的顺序寻找可用路径。",
"local-edge-only-deform": "只改当前Edge:移动被选Edge端点,相邻面自然变斜,整体端面不一起平移。",
"move-edge-end-plane-by-push-pull": "移动端面/保持垂直:端面和端面上的相关边一起移动,相邻平面尽量保持垂直,正方体会更像变成长方体。",
"keep-first-level-planar-relations": "保持关系:保持一级平面平行/垂直关系;只在直接相邻平面和移动端面能验证时执行,不能验证就直接阻止。",
"resize-adjacent-cylinder-from-circular-edge-length": "相邻圆柱直径:把圆Edge长度换算成圆柱直径,优先改孔/槽/凸台局部特征。",
"scale-owning-shape-from-edge": "缩放所属对象:所属特征或 Solid 整体跟随变化,其它尺寸也会变。",
}.get(strategy_mode, "自动选择:程序会在确认窗口显示实际采用的策略。")
@@ -4836,6 +4912,19 @@ class WindowStateMixin:
),
"preselect_combos": edge_length_strategy_preselect("move-edge-end-plane-by-push-pull"),
},
"keep-first-level-planar-relations": {
"label": "保持关系",
"action": "resize_any_edge_length",
"target_attr": "edge_target_length_input",
"enabled": bool(is_line_edge and current_length is not None),
"enabled_tip": "输入当前Edge的目标长度;程序会尝试移动端面,并要求一级平面平行/垂直关系可验证。",
"disabled_tip": "只有直线Edge才能尝试保持一级平面关系;圆Edge请用相邻圆柱,复杂曲线暂未实现。",
"range_hint": (
f"{edge_strategy_effect_text('keep-first-level-planar-relations', current_anchor_mode)} "
f"{relative_range_hint(current_length, 0.25, 0.5)}"
),
"preselect_combos": edge_length_strategy_preselect("keep-first-level-planar-relations"),
},
"resize-adjacent-cylinder-from-circular-edge-length": {
"label": "相邻圆柱",
"action": "resize_any_edge_length",