feat: 完善参数化编辑语义和槽孔中心距

This commit is contained in:
2026-07-30 17:54:01 +08:00
parent c937e13d43
commit 27e4f7236c
22 changed files with 8769 additions and 668 deletions
+156 -19
View File
@@ -744,6 +744,11 @@ class FeatureMixin:
target_to_height_ratio = new_diameter / height_estimate if height_estimate > 1e-9 else ""
cutter_plan = self._bounded_cylinder_cutter_plan(face_id, new_diameter, feature)
fill_plan = self._bounded_cylinder_fill_plan(face_id) if resize_mode == "shrink" else {}
edit_semantics = (
"缩小孔/槽直径:先用同轴圆柱补料封住旧孔壁,再按目标直径同轴重切;保持当前轴线和估算高度。"
if resize_mode == "shrink"
else "扩大孔/槽直径:沿当前圆柱轴线用有限长度圆柱 cutter 切到目标直径;保持当前轴线和估算高度。"
)
return {
"status": readiness["resize_status"],
"risk": readiness["resize_risk"],
@@ -773,6 +778,9 @@ class FeatureMixin:
"same_domain_range_source": axis_range["range_source"],
"material_vote_summary": info.get("material_vote_summary"),
"material_sample_count": info.get("material_sample_count"),
"resize_strategy": "same-axis-bounded-cylinder-recut",
"edit_strategy_label": "同轴圆柱重切",
"edit_semantics": edit_semantics,
**cutter_plan,
**fill_plan,
}
@@ -918,6 +926,8 @@ class FeatureMixin:
"same_domain_v_range": (axis_range.get("v_min"), axis_range.get("v_max")),
"same_domain_range_source": axis_range.get("range_source", ""),
"resize_strategy": "fill-old-cylinder-and-cut-moved-cylinder",
"edit_strategy_label": "填旧孔并切新孔",
"edit_semantics": "先填补当前完整圆柱孔,再按同直径在目标轴心切出新孔;这会改变孔的位置,不会整体平移零件。",
}
def cylindrical_slot_resize_plan(
@@ -1034,6 +1044,8 @@ class FeatureMixin:
"slot_resize_mode": mode_key,
"slot_resize_label": mode_labels.get(mode_key, "slot value"),
"slot_resize_strategy": "local-sector-fixed-angular-span",
"edit_strategy_label": "局部扇形槽重建",
"edit_semantics": "保持当前槽/半孔圆弧角度,把目标宽度、深度或圆弧长度换算成圆柱直径,再填旧槽并切出新的局部扇形槽。",
"slot_kind": slot_kind,
"slot_status": slot_status,
"slot_target_value": target_value,
@@ -1084,7 +1096,11 @@ class FeatureMixin:
resize_plan,
pair_face_id=pair_face_id,
)
edit_strategy_label = "局部扇形槽重建"
edit_semantics = "保持当前槽/半孔圆弧角度,把目标值换算成圆柱直径,再填旧槽并切出新的局部扇形槽。"
if paired_slot_plan:
edit_strategy_label = "配对长圆槽重建"
edit_semantics = "识别到长圆槽另一端后,会填充旧长圆槽包络,再按同一槽中心线和目标宽度重切完整长圆槽。"
risk = _max_risk(risk, str(paired_slot_plan.get("slot_pair_risk", "medium")))
pair_warning = str(paired_slot_plan.get("slot_pair_warning", ""))
if pair_warning:
@@ -1104,6 +1120,8 @@ class FeatureMixin:
**resize_plan,
**slot_plan,
**paired_slot_plan,
"edit_strategy_label": edit_strategy_label,
"edit_semantics": edit_semantics,
"status": status,
"risk": risk,
"message": message,
@@ -1235,6 +1253,8 @@ class FeatureMixin:
"slot_resize_mode": "angular_span",
"slot_resize_label": "slot angular span",
"slot_resize_strategy": "local-sector-angular-span",
"edit_strategy_label": "局部扇形槽角度重建",
"edit_semantics": "保持当前圆柱半径和轴线,围绕当前角度中心填旧扇形槽并切出目标圆弧角度的新扇形槽。",
"resize_mode": "widen" if (delta_span or 0.0) > 0 else "narrow",
"feature_type": feature.get("feature_type"),
"feature_guess": info.get("feature_guess"),
@@ -1388,11 +1408,51 @@ class FeatureMixin:
except Exception as exc:
blockers.append(f"Could not build slot axis move tool plan: {exc}")
resize_strategy = "fill-old-slot-sector-and-cut-moved-sector"
edit_strategy_label = "填旧槽并切新槽"
edit_semantics = "先填补当前槽/半孔扇形区域,再按同宽度、同角度在目标轴心切出新槽;不整体平移零件。"
if pair_plan:
risk = _max_risk(risk, "high")
warnings.append(
"A paired obround slot end was detected; this axis move edits the selected local sector, not the whole CAD slot sketch."
)
center_1 = _tuple_or_none(pair_plan.get("slot_capsule_start_center_1"))
center_2 = _tuple_or_none(pair_plan.get("slot_capsule_start_center_2"))
if center_1 is None or center_2 is None:
risk = _max_risk(risk, "high")
warnings.append(
"A paired obround slot end was detected, but the full-slot centerline could not be derived."
)
else:
pair_plan["slot_capsule_target_start_center_1"] = _tuple_add(center_1, movement)
pair_plan["slot_capsule_target_start_center_2"] = _tuple_add(center_2, movement)
pair_plan["slot_target_center_distance"] = pair_plan.get("slot_pair_axis_distance", "")
pair_plan["slot_current_center_distance"] = pair_plan.get("slot_pair_axis_distance", "")
pair_plan["slot_pair_current_axis_center_1"] = current_center
pair_plan["slot_pair_target_axis_center_1"] = target_center
try:
pair_face_id = int(pair_plan.get("slot_pair_face_id", -1))
pair_surf = BRepAdaptor_Surface(self.faces[pair_face_id])
pair_feature = self.feature_info(pair_face_id)
pair_axis_range = self._cylindrical_axis_range(
pair_face_id,
pair_surf,
_int_values(pair_feature.get("feature_side_face_ids")),
)
pair_cyl = pair_surf.Cylinder()
pair_mid_parameter = (float(pair_axis_range["v_min"]) + float(pair_axis_range["v_max"])) * 0.5
pair_center = _point_tuple(
_point_on_axis(pair_cyl.Axis().Location(), pair_cyl.Axis().Direction(), pair_mid_parameter)
)
pair_plan["slot_pair_current_axis_center_2"] = pair_center
pair_plan["slot_pair_target_axis_center_2"] = _tuple_add(pair_center, movement)
except Exception:
pair_plan["slot_pair_current_axis_center_2"] = ""
pair_plan["slot_pair_target_axis_center_2"] = ""
if not pair_plan.get("slot_pair_target_axis_center_2"):
blockers.append("Could not derive both obround slot end centers for the axis move.")
else:
resize_strategy = "paired-obround-slot-axis-prism"
edit_strategy_label = "整条长圆槽轴心移动"
edit_semantics = "识别到长圆槽另一端后,会填充旧长圆槽包络,再按同槽宽、同总长度在目标轴心重切整条长圆槽;不整体平移零件。"
risk = _max_risk(risk, str(pair_plan.get("slot_pair_risk", "medium")))
warnings.append("Detected paired partial-cylinder slot ends; axis move will relocate the full obround slot.")
current_radius = current_diameter * 0.5 if current_diameter is not None else None
current_width = (
@@ -1412,6 +1472,7 @@ class FeatureMixin:
return {
**cutter_plan,
**fill_plan,
**pair_plan,
"status": status,
"risk": "blocked" if blockers else risk,
"message": message,
@@ -1455,7 +1516,9 @@ class FeatureMixin:
"same_domain_face_count": axis_range.get("same_domain_face_count", 0),
"same_domain_v_range": (axis_range.get("v_min"), axis_range.get("v_max")),
"same_domain_range_source": axis_range.get("range_source", ""),
"resize_strategy": "fill-old-slot-sector-and-cut-moved-sector",
"resize_strategy": resize_strategy,
"edit_strategy_label": edit_strategy_label,
"edit_semantics": edit_semantics,
}
def _paired_obround_slot_plan(
@@ -1767,6 +1830,8 @@ class FeatureMixin:
"slot_resize_mode": "total_length",
"slot_resize_label": "slot total length",
"slot_resize_strategy": "paired-obround-slot-length-prism",
"edit_strategy_label": "长圆槽总长度重建",
"edit_semantics": "保持槽宽不变,围绕长圆槽中心线对两端中心距做对称调整,然后填旧槽并重切目标总长度的长圆槽。",
"resize_mode": "lengthen" if (delta_length or 0.0) > 0 else "shorten",
"slot_current_total_length": current_total_length,
"slot_target_total_length": target_total_length,
@@ -1778,6 +1843,68 @@ class FeatureMixin:
"slot_capsule_target_start_center_2": target_center_2,
}
def cylindrical_slot_center_distance_plan(
self,
face_id: int,
target_center_distance: float,
pair_face_id: int | None = None,
) -> dict[str, object]:
if face_id < 0 or face_id >= len(self.faces):
raise ValueError(f"Unknown face id {face_id}")
info = self.face_info(face_id)
current_diameter = _float_or_none(info.get("diameter"))
if current_diameter is None or current_diameter <= 1e-9:
return {
"status": "blocked",
"risk": "blocked",
"message": "Selected face is not a measurable cylindrical slot face.",
"blockers": "Selected face is not a measurable cylindrical slot face.",
"warnings": "",
}
try:
target_center_distance = float(target_center_distance)
except (TypeError, ValueError):
target_center_distance = 0.0
target_total_length = target_center_distance + current_diameter
plan = self.cylindrical_slot_total_length_plan(
face_id,
target_total_length,
pair_face_id=pair_face_id,
)
current_center_distance = _float_or_none(plan.get("slot_current_center_distance"))
delta = (
None
if current_center_distance is None
else target_center_distance - current_center_distance
)
delta_ratio = (
None
if current_center_distance is None or current_center_distance <= 1e-9 or delta is None
else abs(delta) / current_center_distance
)
if target_center_distance <= 0:
blockers = str(plan.get("blockers") or "")
extra = "Target slot center distance must be greater than 0."
plan["blockers"] = "; ".join(item for item in (blockers, extra) if item)
plan["message"] = plan["blockers"]
plan["status"] = "blocked"
plan["risk"] = "blocked"
plan.update(
{
"slot_resize_mode": "center_distance",
"slot_resize_label": "slot center distance",
"slot_resize_strategy": "paired-obround-slot-center-distance-prism",
"edit_strategy_label": "长圆槽中心距重建",
"edit_semantics": "保持槽宽不变,围绕长圆槽中心线对两端半圆中心距做对称调整,然后填旧槽并重切目标中心距的长圆槽。",
"slot_target_center_distance": target_center_distance,
"slot_target_total_length": target_total_length,
"slot_center_distance_delta": delta,
"slot_center_distance_delta_ratio": delta_ratio,
}
)
return plan
def cylindrical_boss_resize_plan(self, face_id: int, new_diameter: float) -> dict[str, object]:
if face_id < 0 or face_id >= len(self.faces):
raise ValueError(f"Unknown face id {face_id}")
@@ -1806,6 +1933,11 @@ class FeatureMixin:
height_estimate = float(scoped_info.get("height_estimate", 0.0))
target_to_height_ratio = new_diameter / height_estimate if height_estimate > 1e-9 else ""
tool_plan = self._bounded_boss_resize_tool_plan(face_id, new_diameter)
edit_semantics = (
"扩大凸台直径:按当前凸台轴线和估算高度生成目标圆柱补料体,并与所属特征做局部 Fuse。"
if resize_mode == "enlarge"
else "缩小凸台直径:先移除旧凸台包络,再补回目标直径圆柱;这是局部重建,不是缩放整个零件。"
)
return {
"status": readiness["boss_resize_status"],
"risk": readiness["boss_resize_risk"],
@@ -1834,6 +1966,9 @@ class FeatureMixin:
"material_sample_count": info.get("material_sample_count"),
"feature_adjacent_face_ids": feature.get("feature_adjacent_face_ids"),
"feature_boundary_edge_ids": feature.get("feature_boundary_edge_ids"),
"resize_strategy": "bounded-cylindrical-boss-envelope-rebuild",
"edit_strategy_label": "凸台包络重建",
"edit_semantics": edit_semantics,
**tool_plan,
}
@@ -1980,6 +2115,8 @@ class FeatureMixin:
"same_domain_v_range": (axis_range.get("v_min"), axis_range.get("v_max")),
"same_domain_range_source": axis_range.get("range_source", ""),
"resize_strategy": "remove-old-boss-and-fuse-moved-cylinder",
"edit_strategy_label": "移除旧凸台并补新凸台",
"edit_semantics": "先移除当前凸台包络,再按同直径在目标轴心补出新凸台;不整体平移零件。",
}
def cylindrical_suppress_plan(self, face_id: int) -> dict[str, object]:
@@ -2004,6 +2141,8 @@ class FeatureMixin:
scoped_info.update(self._cylinder_end_opening_info(face_id, BRepAdaptor_Surface(self.faces[face_id]), axis_range))
readiness = _cylinder_suppress_readiness(scoped_info)
fill_plan = self._bounded_cylinder_fill_plan(face_id)
fill_plan["fill_strategy"] = "bounded-hole-suppress-fill"
fill_plan["fill_note"] = "按当前圆柱孔范围生成略带重叠的补料圆柱体,用于封堵完整通孔或盲孔。"
return {
"status": readiness["suppress_status"],
"risk": readiness["suppress_risk"],
@@ -2029,6 +2168,9 @@ class FeatureMixin:
"feature_bottom_face_ids": feature.get("feature_bottom_face_ids"),
"feature_opening_face_ids": feature.get("feature_opening_face_ids"),
"feature_bottom_note": feature.get("feature_bottom_note"),
"resize_strategy": "fill-cylindrical-hole-volume",
"edit_strategy_label": "圆柱补料封堵",
"edit_semantics": "按当前孔轴线和估算高度生成补料圆柱体,局部 Fuse 后封堵当前完整圆柱孔。",
**fill_plan,
}
@@ -2106,6 +2248,9 @@ class FeatureMixin:
"feature_bottom_confidence": feature.get("feature_bottom_confidence"),
"feature_bottom_detection": feature.get("feature_bottom_detection"),
"feature_bottom_note": feature.get("feature_bottom_note"),
"resize_strategy": "bounded-blind-depth-cut-or-fill",
"edit_strategy_label": "盲孔/盲槽深度切削或补料",
"edit_semantics": "沿识别到的开口到底面方向调整深度:加深时切削,变浅时从新底面到旧底面补料。",
}
plan.update(context)
return plan
@@ -2407,23 +2552,14 @@ class FeatureMixin:
old_radius = cyl.Radius()
new_radius = new_diameter / 2.0
axis_range = self._cylindrical_axis_range(face_id, surf)
end_info = self._cylinder_end_opening_info(face_id, surf, axis_range)
v_min = float(axis_range["v_min"])
v_max = float(axis_range["v_max"])
span = max(v_max - v_min, 1e-6)
delta_radius = abs(new_radius - old_radius)
base_radius = max(old_radius, new_radius)
resize_mode = _resize_mode(old_radius * 2.0, new_diameter)
if resize_mode == "enlarge":
axial_margin = 0.0
start_parameter = v_min
end_parameter = v_max
else:
axial_margin = min(
max(base_radius * 0.001, delta_radius * 0.01, span * 0.001, 0.001),
max(span * 0.01, 0.02),
)
start_parameter = v_min - axial_margin
end_parameter = v_max + axial_margin
axial_margin = 0.0
start_parameter = v_min
end_parameter = v_max
radial_overlap = min(max(old_radius * 0.001, 0.001), 0.05)
height = max(end_parameter - start_parameter, 1e-6)
axis = cyl.Axis()
@@ -2436,7 +2572,7 @@ class FeatureMixin:
"boss_tool_note": (
"扩大凸台会在同域圆柱侧壁整体 V 范围内生成目标半径圆柱并 Fuse;"
"缩小凸台会先用旧半径包络体移除原凸台范围,再 Fuse 目标半径圆柱重建。"
"当前版本会给轴向两端保留少量重叠,让布尔结果更容易和原实体合并"
"补新凸台时使用原凸台轴向范围,避免直径或轴心修改时悄悄改变高度"
),
"boss_tool_scope_face_ids": axis_range["same_domain_face_ids"],
"boss_tool_scope_face_count": axis_range["same_domain_face_count"],
@@ -2455,6 +2591,7 @@ class FeatureMixin:
"boss_tool_start_point": _point_tuple(start),
"boss_tool_exact_start_point": _point_tuple(exact_start),
"boss_tool_exact_height": max(v_max - v_min, 1e-6),
**end_info,
}
def _cylinder_end_opening_info(