feat: 完善 Face 一级关系编辑和稳定性校验
This commit is contained in:
@@ -214,6 +214,9 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.edit_thread: QThread | None = None
|
||||
self.edit_worker: EditWorker | None = None
|
||||
self.pending_edit_context: dict[str, object] | None = None
|
||||
self.active_isolated_edit_process = None
|
||||
self.isolated_edit_cancel_requested = False
|
||||
self.close_after_edit_cancel = False
|
||||
self.scan_in_progress = False
|
||||
self.scan_thread: QThread | None = None
|
||||
self.scan_worker: ScanWorker | None = None
|
||||
|
||||
@@ -64,7 +64,41 @@ from .geometry_utils import * # noqa: F403
|
||||
|
||||
|
||||
class FeatureMixin:
|
||||
def _first_level_fact_plan_fields(self, face_id: int, scope: str) -> dict[str, object]:
|
||||
try:
|
||||
return self.face_first_level_facts(face_id, scope=scope)
|
||||
except Exception as exc:
|
||||
resolved_scope = str(scope or "face")
|
||||
return {
|
||||
"first_level_fact_model": "STEP/B-Rep first-level fact graph",
|
||||
"first_level_fact_source_model": resolved_scope,
|
||||
"first_level_fact_status": "unavailable",
|
||||
"first_level_fact_relation_depth": 1,
|
||||
"first_level_fact_relation_boundary": "shared-edge",
|
||||
"first_level_fact_scope": resolved_scope,
|
||||
"first_level_fact_subject_role": "selected Face",
|
||||
"first_level_fact_subject_face_ids": (face_id,),
|
||||
"first_level_fact_subject_face_count": 1,
|
||||
"first_level_fact_boundary_edge_ids": (),
|
||||
"first_level_fact_boundary_edge_count": 0,
|
||||
"first_level_fact_boundary_vertex_points": (),
|
||||
"first_level_fact_boundary_vertex_count": 0,
|
||||
"first_level_fact_adjacent_face_ids": (),
|
||||
"first_level_fact_adjacent_face_count": 0,
|
||||
"first_level_fact_adjacent_surface_types": (),
|
||||
"first_level_fact_shared_edges_by_face": (),
|
||||
"first_level_fact_included_face_ids": (face_id,),
|
||||
"first_level_fact_included_face_count": 1,
|
||||
"first_level_fact_role_groups": (),
|
||||
"first_level_fact_ignored_relation_depths": ("second-level", "third-level", "deeper"),
|
||||
"first_level_fact_ignored_relation_note": (
|
||||
"Only first-level shared-edge relations are considered in this stage."
|
||||
),
|
||||
"first_level_fact_summary": f"当前 Face 的一级事实图暂时无法生成:{exc}",
|
||||
}
|
||||
|
||||
def _cylindrical_feature_first_level_plan_fields(self, face_id: int) -> dict[str, object]:
|
||||
fact_fields = self._first_level_fact_plan_fields(face_id, "cylindrical-feature")
|
||||
try:
|
||||
topology = self.cylindrical_feature_first_level_topology(face_id)
|
||||
except Exception as exc:
|
||||
@@ -80,6 +114,7 @@ class FeatureMixin:
|
||||
"first_level_adjacent_face_count": 0,
|
||||
"cylindrical_feature_side_face_ids": (face_id,),
|
||||
"cylindrical_feature_side_face_count": 1,
|
||||
**fact_fields,
|
||||
}
|
||||
|
||||
fields = {
|
||||
@@ -121,6 +156,7 @@ class FeatureMixin:
|
||||
0,
|
||||
),
|
||||
}
|
||||
fields.update(fact_fields)
|
||||
fields["first_level_edit_semantics"] = (
|
||||
"Cylindrical feature edits currently use only first-level shared-edge topology: "
|
||||
"the selected cylinder/slot wall is rebuilt together with its direct boundary neighbors; "
|
||||
@@ -2394,6 +2430,8 @@ class FeatureMixin:
|
||||
"solid_id": info["solid_id"],
|
||||
"diameter": info.get("diameter"),
|
||||
"radius": info.get("radius"),
|
||||
"axis": info.get("axis"),
|
||||
"axis_point": info.get("axis_point"),
|
||||
"angular_span": info.get("angular_span"),
|
||||
"height_estimate": scoped_info.get("height_estimate"),
|
||||
"feature_type": feature.get("feature_type"),
|
||||
@@ -3067,11 +3105,110 @@ class FeatureMixin:
|
||||
)
|
||||
return samples
|
||||
|
||||
def _cylindrical_cap_push_pull_direction(
|
||||
self,
|
||||
face_id: int,
|
||||
surf: BRepAdaptor_Surface,
|
||||
) -> dict[str, object] | None:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
return None
|
||||
if surf.GetType() != GeomAbs_Plane:
|
||||
return None
|
||||
|
||||
plane_point = surf.Plane().Location()
|
||||
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
|
||||
adjacent_face_ids = self._adjacent_face_ids_for_edges(boundary_edge_ids, face_id)
|
||||
if not adjacent_face_ids:
|
||||
return None
|
||||
|
||||
candidates: list[tuple[float, tuple[float, float, float], dict[str, object]]] = []
|
||||
diagonal = _shape_diagonal(self.shape)
|
||||
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
|
||||
for adjacent_id in adjacent_face_ids:
|
||||
if adjacent_id < 0 or adjacent_id >= len(self.faces):
|
||||
continue
|
||||
try:
|
||||
side_surf = BRepAdaptor_Surface(self.faces[adjacent_id])
|
||||
except Exception:
|
||||
continue
|
||||
if side_surf.GetType() != GeomAbs_Cylinder:
|
||||
continue
|
||||
|
||||
cylinder = side_surf.Cylinder()
|
||||
radius = max(float(cylinder.Radius()), 0.0)
|
||||
axis = cylinder.Axis()
|
||||
axis_point = axis.Location()
|
||||
axis_dir = axis.Direction()
|
||||
try:
|
||||
axis_range = self._cylindrical_axis_range(adjacent_id, side_surf)
|
||||
except Exception:
|
||||
axis_range = {
|
||||
"v_min": min(float(side_surf.FirstVParameter()), float(side_surf.LastVParameter())),
|
||||
"v_max": max(float(side_surf.FirstVParameter()), float(side_surf.LastVParameter())),
|
||||
}
|
||||
v_min = float(axis_range["v_min"])
|
||||
v_max = float(axis_range["v_max"])
|
||||
height = max(v_max - v_min, 1e-9)
|
||||
cap_parameter = _axis_parameter(axis_point, axis_dir, plane_point)
|
||||
start_distance = abs(cap_parameter - v_min)
|
||||
end_distance = abs(cap_parameter - v_max)
|
||||
end_tolerance = max(height * 0.05, radius * 0.2, tolerance * 10.0, 0.05)
|
||||
if start_distance <= end_distance and start_distance <= end_tolerance:
|
||||
outward = _neg_tuple(_dir_tuple(axis_dir))
|
||||
end_label = "start"
|
||||
score = start_distance
|
||||
elif end_distance <= end_tolerance:
|
||||
outward = _dir_tuple(axis_dir)
|
||||
end_label = "end"
|
||||
score = end_distance
|
||||
else:
|
||||
continue
|
||||
candidates.append(
|
||||
(
|
||||
score,
|
||||
outward,
|
||||
{
|
||||
"cap_axis_face_id": adjacent_id,
|
||||
"cap_axis_end": end_label,
|
||||
"cap_axis_parameter": cap_parameter,
|
||||
"cap_axis_start_parameter": v_min,
|
||||
"cap_axis_end_parameter": v_max,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
candidates.sort(key=lambda item: item[0])
|
||||
_score, outward, details = candidates[0]
|
||||
for _other_score, other_outward, _other_details in candidates[1:]:
|
||||
if _tuple_dot(outward, other_outward) < 0.92:
|
||||
return None
|
||||
return {
|
||||
"outward_direction": outward,
|
||||
"inward_direction": _neg_tuple(outward),
|
||||
"plus_side_state": "cylindrical-cap-axis",
|
||||
"minus_side_state": "cylindrical-cap-axis",
|
||||
"confidence": "high",
|
||||
"note": "cylindrical cap direction inferred from adjacent cylinder axis",
|
||||
"push_pull_outward_direction": outward,
|
||||
"push_pull_inward_direction": _neg_tuple(outward),
|
||||
"push_pull_plus_side": "cylindrical-cap-axis",
|
||||
"push_pull_minus_side": "cylindrical-cap-axis",
|
||||
"push_pull_confidence": "high",
|
||||
"push_pull_note": "cylindrical cap direction inferred from adjacent cylinder axis",
|
||||
**details,
|
||||
}
|
||||
|
||||
def _plane_push_pull_direction(self, face_id: int, surf: BRepAdaptor_Surface) -> dict[str, object]:
|
||||
face = self.faces[face_id]
|
||||
direction = surf.Plane().Axis().Direction()
|
||||
axis_tuple = _dir_tuple(direction)
|
||||
oriented_tuple = _oriented_dir_tuple(direction, face)
|
||||
cap_direction = self._cylindrical_cap_push_pull_direction(face_id, surf)
|
||||
if cap_direction is not None:
|
||||
return cap_direction
|
||||
fallback = {
|
||||
"outward_direction": oriented_tuple,
|
||||
"inward_direction": _neg_tuple(oriented_tuple),
|
||||
|
||||
@@ -33,6 +33,12 @@ def _execute(model: StepModel, operation: str, args: list[object]) -> str:
|
||||
return model.resize_shell_thickness(int(args[0]), float(args[1]))
|
||||
if operation == "resize_shell_thickness_owning_scale":
|
||||
return model.resize_shell_thickness_owning_scale(int(args[0]), float(args[1]))
|
||||
if operation == "resize_cylindrical_height":
|
||||
return model.resize_cylindrical_height(int(args[0]), float(args[1]))
|
||||
if operation == "resize_cylindrical_boss_height":
|
||||
return model.resize_cylindrical_boss_height(int(args[0]), float(args[1]))
|
||||
if operation == "resize_cylindrical_height_owning_scale":
|
||||
return model.resize_cylindrical_height_owning_scale(int(args[0]), float(args[1]))
|
||||
if operation == "resize_cone_reference_radius":
|
||||
return model.resize_conical_reference_radius(int(args[0]), float(args[1]))
|
||||
if operation == "resize_cone_semi_angle":
|
||||
|
||||
+441
-32
@@ -113,6 +113,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._same_domain_face_ids_cache: dict[int, list[int]] = {}
|
||||
self._face_first_level_topology_cache: dict[int, dict[str, object]] = {}
|
||||
self._cylindrical_first_level_topology_cache: dict[int, dict[str, object]] = {}
|
||||
self._face_first_level_fact_cache: dict[tuple[int, str], dict[str, object]] = {}
|
||||
self._local_face_deform_readiness_cache: dict[int, dict[str, object]] = {}
|
||||
self._edge_duplicate_key_ids_cache: dict[tuple[object, ...], list[int]] | None = None
|
||||
self._same_domain_internal_edge_ids_cache: set[int] | None = None
|
||||
@@ -196,6 +197,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._same_domain_face_ids_cache.clear()
|
||||
self._face_first_level_topology_cache.clear()
|
||||
self._cylindrical_first_level_topology_cache.clear()
|
||||
self._face_first_level_fact_cache.clear()
|
||||
self._local_face_deform_readiness_cache.clear()
|
||||
self._edge_duplicate_key_ids_cache = None
|
||||
self._same_domain_internal_edge_ids_cache = None
|
||||
@@ -290,6 +292,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._same_domain_face_ids_cache.clear()
|
||||
self._face_first_level_topology_cache.clear()
|
||||
self._cylindrical_first_level_topology_cache.clear()
|
||||
self._face_first_level_fact_cache.clear()
|
||||
self._local_face_deform_readiness_cache.clear()
|
||||
|
||||
def _restore_face_logical_ids_if_count_matches(self, logical_ids: Iterable[int]) -> bool:
|
||||
@@ -303,6 +306,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._same_domain_face_ids_cache.clear()
|
||||
self._face_first_level_topology_cache.clear()
|
||||
self._cylindrical_first_level_topology_cache.clear()
|
||||
self._face_first_level_fact_cache.clear()
|
||||
self._local_face_deform_readiness_cache.clear()
|
||||
return True
|
||||
|
||||
@@ -349,12 +353,23 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
info["oriented_normal"] = _oriented_dir_tuple(direction, face)
|
||||
info["push_pull_confidence"] = "unchecked"
|
||||
info["push_pull_note"] = "快速选择阶段不判断材料内外方向;执行推拉时会重新计算。"
|
||||
cap_direction = self._cylindrical_cap_push_pull_direction(face_id, surf)
|
||||
if cap_direction is not None:
|
||||
info.update(cap_direction)
|
||||
info["push_pull_status"] = "candidate"
|
||||
info["feature_type"] = "可推拉平面候选"
|
||||
info["feature_source_face_id"] = face_id
|
||||
info["feature_highlight_face_ids"] = (face_id,)
|
||||
info["feature_edit_actions"] = "推拉平面"
|
||||
info.update(self._local_face_deform_readiness(face_id))
|
||||
if bool(info.get("has_inner_boundaries")):
|
||||
info["local_face_deform_ready"] = False
|
||||
info["local_face_deform_face_count"] = 1
|
||||
info["local_face_deform_blocker"] = "当前 Face 有内孔/内边界;请优先使用推拉当前面、孔或槽的专门修改入口。"
|
||||
else:
|
||||
info["local_face_deform_ready"] = True
|
||||
info["local_face_deform_face_count"] = 1
|
||||
info["local_face_deform_blocker"] = ""
|
||||
info["local_face_deform_status"] = "deferred"
|
||||
info.update(
|
||||
self._local_face_plane_size_info(
|
||||
face_id,
|
||||
@@ -416,6 +431,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
info["feature_torus_major_radius"] = torus.MajorRadius()
|
||||
info["feature_torus_minor_radius"] = torus.MinorRadius()
|
||||
|
||||
info.update(self._recognition_summary_fields(info))
|
||||
self._quick_face_info_cache[face_id] = dict(info)
|
||||
return dict(info)
|
||||
|
||||
@@ -512,6 +528,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
info["axis"] = _dir_tuple(torus.Axis().Direction())
|
||||
info["major_radius"] = torus.MajorRadius()
|
||||
info["minor_radius"] = torus.MinorRadius()
|
||||
info.update(self._recognition_summary_fields(info))
|
||||
self._face_info_cache[face_id] = dict(info)
|
||||
return dict(info)
|
||||
|
||||
@@ -545,6 +562,267 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _recognition_summary_fields(self, info: dict[str, object]) -> dict[str, object]:
|
||||
surface = str(info.get("surface") or "")
|
||||
candidate = (
|
||||
str(info.get("feature_type") or "").strip()
|
||||
or str(info.get("feature_guess") or "").strip()
|
||||
or (f"{surface} Face" if surface else "Face")
|
||||
)
|
||||
confidence = (
|
||||
str(info.get("confidence") or "").strip()
|
||||
or str(info.get("push_pull_confidence") or "").strip()
|
||||
or str(info.get("shell_confidence") or "").strip()
|
||||
or "unchecked"
|
||||
)
|
||||
if confidence == "unchecked":
|
||||
feature_guess = str(info.get("feature_guess") or "")
|
||||
has_axis = info.get("axis") not in {None, ""} or info.get("axis_point") not in {None, ""}
|
||||
if surface == "plane" and info.get("boundary_edges") not in {None, ""}:
|
||||
confidence = "high"
|
||||
elif surface == "cylinder" and _float_or_none(info.get("radius")) is not None and has_axis:
|
||||
confidence = "high" if feature_guess else "medium"
|
||||
elif surface == "cone" and _float_or_none(info.get("reference_radius")) is not None and _float_or_none(info.get("semi_angle")) is not None:
|
||||
confidence = "medium"
|
||||
elif surface == "sphere" and _float_or_none(info.get("radius")) is not None:
|
||||
confidence = "high"
|
||||
elif surface == "torus" and _float_or_none(info.get("major_radius")) is not None and _float_or_none(info.get("minor_radius")) is not None:
|
||||
confidence = "high"
|
||||
|
||||
risk_rank = {"low": 0, "medium": 1, "high": 2, "blocked": 3}
|
||||
capability_specs = (
|
||||
("resize_status", "resize_risk", "resize_blockers", "孔/槽/圆柱直径"),
|
||||
("push_pull_status", "push_pull_risk", "push_pull_blockers", "平面推拉"),
|
||||
("shell_status", "shell_risk", "shell_blockers", "薄壁厚度"),
|
||||
("cylinder_resize_status", "cylinder_resize_risk", "cylinder_resize_blockers", "圆柱直径/半径"),
|
||||
("boss_resize_status", "boss_resize_risk", "boss_resize_blockers", "圆柱凸台直径/高度/轴心"),
|
||||
("depth_status", "depth_risk", "depth_blockers", "盲孔/盲槽深度"),
|
||||
("suppress_status", "suppress_risk", "suppress_blockers", "封堵孔/槽"),
|
||||
("fillet_status", "fillet_risk", "fillet_blockers", "已有圆角半径"),
|
||||
("chamfer_status", "chamfer_risk", "chamfer_blockers", "倒角"),
|
||||
)
|
||||
available_capabilities = {
|
||||
status_key
|
||||
for status_key, _risk_key, _blocker_key, _label in capability_specs
|
||||
if str(info.get(status_key) or "").strip() in {"ready", "caution", "candidate"}
|
||||
}
|
||||
feature_type_text = str(info.get("feature_type") or "")
|
||||
feature_actions_text = str(info.get("feature_edit_actions") or "")
|
||||
has_planar_push_pull_candidate = (
|
||||
surface == "plane"
|
||||
and (
|
||||
"推拉" in feature_type_text
|
||||
or "推拉" in feature_actions_text
|
||||
or str(info.get("push_pull_status") or "").strip() == "candidate"
|
||||
)
|
||||
)
|
||||
has_local_face_deform = bool(info.get("local_face_deform_ready"))
|
||||
has_slot_candidate = str(info.get("slot_status") or "").strip() == "candidate"
|
||||
has_shell_candidate = str(info.get("shell_region_status") or "").strip() == "candidate"
|
||||
has_analytic_surface_candidate = surface in {"cone", "sphere", "torus"} and bool(feature_type_text)
|
||||
has_available_capability = bool(
|
||||
available_capabilities
|
||||
or has_planar_push_pull_candidate
|
||||
or has_local_face_deform
|
||||
or has_slot_candidate
|
||||
or has_shell_candidate
|
||||
or has_analytic_surface_candidate
|
||||
)
|
||||
risk = "low"
|
||||
for key in (
|
||||
"risk",
|
||||
"first_level_topology_risk",
|
||||
):
|
||||
value = str(info.get(key) or "").strip()
|
||||
if risk_rank.get(value, -1) > risk_rank.get(risk, -1):
|
||||
risk = value
|
||||
for status_key, risk_key, _blocker_key, _label in capability_specs:
|
||||
value = str(info.get(risk_key) or "").strip()
|
||||
if value == "blocked" and has_available_capability:
|
||||
continue
|
||||
if risk_rank.get(value, -1) > risk_rank.get(risk, -1):
|
||||
risk = value
|
||||
if confidence in {"low", "unchecked", "none"} and risk == "low":
|
||||
risk = "medium"
|
||||
|
||||
evidence: list[str] = []
|
||||
evidence_keys: list[str] = []
|
||||
|
||||
def add(key: str, text: str) -> None:
|
||||
if text and key not in evidence_keys:
|
||||
evidence_keys.append(key)
|
||||
evidence.append(text)
|
||||
|
||||
if surface:
|
||||
add("surface", f"曲面={surface}")
|
||||
if surface == "cylinder" and info.get("radius") not in {None, ""}:
|
||||
add("cylinder_geometry", f"圆柱半径={info.get('radius')}")
|
||||
if surface == "cone":
|
||||
if info.get("reference_radius") not in {None, ""}:
|
||||
add("cone_geometry", f"圆锥参考半径={info.get('reference_radius')}")
|
||||
if info.get("semi_angle") not in {None, ""}:
|
||||
add("cone_angle", f"圆锥半角={info.get('semi_angle')}")
|
||||
if surface == "sphere" and info.get("radius") not in {None, ""}:
|
||||
add("sphere_geometry", f"球半径={info.get('radius')}")
|
||||
if surface == "torus":
|
||||
if info.get("major_radius") not in {None, ""}:
|
||||
add("torus_major_radius", f"环面主半径={info.get('major_radius')}")
|
||||
if info.get("minor_radius") not in {None, ""}:
|
||||
add("torus_minor_radius", f"环面小半径={info.get('minor_radius')}")
|
||||
if info.get("boundary_edges") not in {None, ""}:
|
||||
add("boundary_edges", f"边界Edge={info.get('boundary_edges')}")
|
||||
if info.get("same_domain_face_count") not in {None, ""}:
|
||||
add("same_domain", f"同域Face={info.get('same_domain_face_count')}")
|
||||
if info.get("first_level_adjacent_face_count") not in {None, ""}:
|
||||
add("first_level_topology", f"一级相邻Face={info.get('first_level_adjacent_face_count')}")
|
||||
elif info.get("feature_adjacent_face_ids") not in {None, ""}:
|
||||
try:
|
||||
adjacent_count = len(tuple(info.get("feature_adjacent_face_ids") or ()))
|
||||
except TypeError:
|
||||
adjacent_count = 0
|
||||
add("first_level_topology", f"一级相邻Face={adjacent_count}")
|
||||
if info.get("first_level_fact_summary") not in {None, ""}:
|
||||
add("first_level_fact_graph", f"一级事实={info.get('first_level_fact_summary')}")
|
||||
if info.get("material_vote_summary") not in {None, ""}:
|
||||
add("material_votes", f"材料采样={info.get('material_vote_summary')}")
|
||||
if info.get("feature_end_face_ids") not in {None, ""}:
|
||||
try:
|
||||
end_count = len(tuple(info.get("feature_end_face_ids") or ()))
|
||||
except TypeError:
|
||||
end_count = 0
|
||||
add("end_faces", f"端面Face={end_count}")
|
||||
if info.get("feature_bottom_face_ids") not in {None, ""}:
|
||||
try:
|
||||
bottom_count = len(tuple(info.get("feature_bottom_face_ids") or ()))
|
||||
except TypeError:
|
||||
bottom_count = 0
|
||||
add("bottom_faces", f"疑似底面Face={bottom_count}")
|
||||
if info.get("slot_status") == "candidate":
|
||||
add("slot_geometry", "部分圆柱槽/半孔几何")
|
||||
if info.get("shell_region_status") == "candidate":
|
||||
add("shell_opposite_face", "找到相对平面/薄壁候选")
|
||||
|
||||
ready_actions: list[str] = []
|
||||
limited_actions: list[str] = []
|
||||
blockers: list[str] = []
|
||||
limitations: list[str] = []
|
||||
|
||||
def add_unique(items: list[str], text: str) -> None:
|
||||
if text and text not in items:
|
||||
items.append(text)
|
||||
|
||||
def add_action(items: list[str], text: str) -> None:
|
||||
if text and text not in items:
|
||||
items.append(text)
|
||||
|
||||
for status_key, _risk_key, blocker_key, label in capability_specs:
|
||||
status = str(info.get(status_key) or "").strip()
|
||||
blocker_text = str(info.get(blocker_key) or "").strip()
|
||||
if status in {"ready", "caution", "candidate"}:
|
||||
add_action(ready_actions, label)
|
||||
elif status == "blocked" and blocker_text:
|
||||
add_action(limited_actions, label)
|
||||
|
||||
if has_planar_push_pull_candidate:
|
||||
add_action(ready_actions, "平面推拉")
|
||||
if has_local_face_deform:
|
||||
add_action(ready_actions, "当前面面积/面宽/面高/中心/偏移")
|
||||
elif str(info.get("local_face_deform_blocker") or "").strip():
|
||||
add_action(limited_actions, "当前面局部尺寸/中心/偏移")
|
||||
if has_shell_candidate:
|
||||
add_action(ready_actions, "薄壁厚度")
|
||||
if has_slot_candidate:
|
||||
add_action(ready_actions, "槽/半孔宽度/深度/弧长")
|
||||
if surface == "cone" and feature_type_text:
|
||||
add_action(ready_actions, "圆锥参考半径/直径/半角")
|
||||
elif surface == "sphere" and feature_type_text:
|
||||
add_action(ready_actions, "球面半径/直径")
|
||||
elif surface == "torus" and feature_type_text:
|
||||
add_action(ready_actions, "环面主/小半径或直径")
|
||||
|
||||
for key in ("local_face_deform_blocker", "first_level_topology_blockers"):
|
||||
text = str(info.get(key) or "").strip()
|
||||
if not text:
|
||||
continue
|
||||
if has_available_capability:
|
||||
add_unique(limitations, text)
|
||||
else:
|
||||
add_unique(blockers, text)
|
||||
for status_key, _risk_key, blocker_key, _label in capability_specs:
|
||||
text = str(info.get(blocker_key) or "").strip()
|
||||
if not text:
|
||||
continue
|
||||
status = str(info.get(status_key) or "").strip()
|
||||
if status == "blocked" and has_available_capability:
|
||||
add_unique(limitations, text)
|
||||
else:
|
||||
add_unique(blockers, text)
|
||||
|
||||
note = (
|
||||
str(info.get("feature_mode") or "").strip()
|
||||
or str(info.get("note") or "").strip()
|
||||
or str(info.get("push_pull_note") or "").strip()
|
||||
)
|
||||
if note:
|
||||
add("note", note)
|
||||
|
||||
confidence_points = {"high": 72, "medium": 56, "low": 34, "unchecked": 22, "none": 0}
|
||||
risk_penalty = {"low": 0, "medium": 14, "high": 30, "blocked": 72}
|
||||
score = confidence_points.get(confidence, 22)
|
||||
score += min(len(evidence_keys) * 5, 24)
|
||||
score -= risk_penalty.get(risk, 14)
|
||||
if blockers:
|
||||
score -= 35
|
||||
elif limitations:
|
||||
score -= min(len(limitations) * 4, 12)
|
||||
score = max(0, min(100, int(round(score))))
|
||||
|
||||
if risk == "blocked" or blockers:
|
||||
decision = "已阻止"
|
||||
elif score >= 76 and risk == "low":
|
||||
decision = "高可信候选"
|
||||
elif score >= 56:
|
||||
decision = "可尝试候选"
|
||||
elif score >= 36:
|
||||
decision = "需人工确认"
|
||||
else:
|
||||
decision = "不建议自动修改"
|
||||
|
||||
summary_parts = [candidate, f"置信度={confidence}", f"风险={risk}", f"评分={score}", f"结论={decision}"]
|
||||
if evidence:
|
||||
summary_evidence = list(evidence[:5])
|
||||
if "first_level_fact_graph" in evidence_keys:
|
||||
fact_text = evidence[evidence_keys.index("first_level_fact_graph")]
|
||||
if fact_text not in summary_evidence:
|
||||
if len(summary_evidence) >= 5:
|
||||
summary_evidence[-1] = fact_text
|
||||
else:
|
||||
summary_evidence.append(fact_text)
|
||||
summary_parts.append("证据:" + ";".join(summary_evidence))
|
||||
if ready_actions:
|
||||
summary_parts.append("可改:" + ";".join(ready_actions[:4]))
|
||||
if blockers:
|
||||
summary_parts.append("限制:" + ";".join(blockers[:2]))
|
||||
elif limitations:
|
||||
summary_parts.append("受限能力:" + ";".join(limitations[:2]))
|
||||
if limited_actions:
|
||||
summary_parts.append("受限修改:" + ";".join(limited_actions[:4]))
|
||||
|
||||
return {
|
||||
"recognition_candidate": candidate,
|
||||
"recognition_confidence": confidence,
|
||||
"recognition_risk": risk,
|
||||
"recognition_score": score,
|
||||
"recognition_decision": decision,
|
||||
"recognition_evidence": ";".join(evidence),
|
||||
"recognition_evidence_keys": tuple(evidence_keys),
|
||||
"recognition_ready_actions": ";".join(ready_actions),
|
||||
"recognition_limited_actions": ";".join(limited_actions),
|
||||
"recognition_blockers": ";".join(blockers),
|
||||
"recognition_limitations": ";".join(limitations),
|
||||
"recognition_summary": ";".join(summary_parts),
|
||||
}
|
||||
|
||||
def cached_feature_info(self, face_id: int) -> dict[str, object] | None:
|
||||
cached = self._feature_info_cache.get(face_id)
|
||||
return dict(cached) if cached is not None else None
|
||||
@@ -584,6 +862,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
),
|
||||
}
|
||||
)
|
||||
result.update(self._recognition_summary_fields(result))
|
||||
self._feature_info_cache[face_id] = dict(result)
|
||||
return dict(result)
|
||||
|
||||
@@ -702,11 +981,9 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
{
|
||||
"kind": "feature",
|
||||
"feature_type": "环面候选",
|
||||
"feature_type": prismatic_info.get("feature_type", "可推拉平面候选"),
|
||||
"feature_source_face_id": face_id,
|
||||
"feature_face_ids": (face_id,),
|
||||
"feature_highlight_face_ids": (face_id,),
|
||||
"feature_highlight_face_ids": tuple(sorted(highlight_face_ids)),
|
||||
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
|
||||
"feature_edit_actions": "修改环面主半径/小半径",
|
||||
"feature_mode": (
|
||||
@@ -1824,6 +2101,133 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._cylindrical_first_level_topology_cache[item] = dict(topology)
|
||||
return dict(topology)
|
||||
|
||||
def face_first_level_facts(self, face_id: int, scope: str = "auto") -> dict[str, object]:
|
||||
"""Return a unified first-level fact graph for recognition and UI.
|
||||
|
||||
This is intentionally a fact layer, not a feature-history guess. It
|
||||
normalizes the selected region, boundary Edges/Vertices, direct
|
||||
shared-edge neighbors, and ignored deeper relation depths so Face,
|
||||
cylinder, hole, slot, and later feature recognizers can share the same
|
||||
evidence contract.
|
||||
"""
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
raise ValueError(f"Unknown face id {face_id}")
|
||||
requested_scope = str(scope or "auto")
|
||||
surface = self.face_surface_kind(face_id)
|
||||
resolved_scope = requested_scope
|
||||
if requested_scope == "auto":
|
||||
resolved_scope = "cylindrical-feature" if surface == "cylinder" else "face"
|
||||
if resolved_scope not in {"face", "cylindrical-feature"}:
|
||||
raise ValueError(f"Unknown first-level fact scope: {scope}")
|
||||
cache_key = (int(face_id), resolved_scope)
|
||||
cached = self._face_first_level_fact_cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return dict(cached)
|
||||
|
||||
if resolved_scope == "cylindrical-feature":
|
||||
topology = self.cylindrical_feature_first_level_topology(face_id)
|
||||
subject_face_ids = tuple(_int_values(topology.get("cylindrical_feature_side_face_ids"))) or (face_id,)
|
||||
boundary_edge_ids = tuple(_int_values(topology.get("cylindrical_feature_boundary_edge_ids")))
|
||||
boundary_vertex_points = tuple(topology.get("cylindrical_feature_boundary_vertex_points") or ())
|
||||
adjacent_face_ids = tuple(_int_values(topology.get("cylindrical_feature_adjacent_face_ids")))
|
||||
included_face_ids = tuple(_int_values(topology.get("cylindrical_feature_first_level_face_ids"))) or tuple(
|
||||
sorted({*subject_face_ids, *adjacent_face_ids})
|
||||
)
|
||||
role_groups = (
|
||||
{
|
||||
"role": "cylindrical-side",
|
||||
"face_ids": subject_face_ids,
|
||||
"count": len(subject_face_ids),
|
||||
},
|
||||
{
|
||||
"role": "direct-adjacent",
|
||||
"face_ids": adjacent_face_ids,
|
||||
"count": len(adjacent_face_ids),
|
||||
},
|
||||
{
|
||||
"role": "end/opening",
|
||||
"face_ids": tuple(_int_values(topology.get("cylindrical_feature_end_face_ids"))),
|
||||
"count": int(topology.get("cylindrical_feature_end_face_count", 0) or 0),
|
||||
},
|
||||
{
|
||||
"role": "blind-bottom",
|
||||
"face_ids": tuple(_int_values(topology.get("cylindrical_feature_bottom_face_ids"))),
|
||||
"count": int(topology.get("cylindrical_feature_bottom_face_count", 0) or 0),
|
||||
},
|
||||
{
|
||||
"role": "slot-boundary",
|
||||
"face_ids": tuple(_int_values(topology.get("cylindrical_feature_slot_boundary_face_ids"))),
|
||||
"count": int(topology.get("cylindrical_feature_slot_boundary_face_count", 0) or 0),
|
||||
},
|
||||
)
|
||||
subject_role = "cylindrical side region"
|
||||
source_model = "cylindrical-feature"
|
||||
else:
|
||||
topology = self.face_first_level_topology(face_id)
|
||||
subject_face_ids = tuple(_int_values(topology.get("same_domain_face_ids"))) or (face_id,)
|
||||
boundary_edge_ids = tuple(_int_values(topology.get("first_level_boundary_edge_ids")))
|
||||
boundary_vertex_points = tuple(topology.get("first_level_boundary_vertex_points") or ())
|
||||
adjacent_face_ids = tuple(_int_values(topology.get("first_level_adjacent_face_ids")))
|
||||
included_face_ids = tuple(_int_values(topology.get("first_level_face_ids"))) or tuple(
|
||||
sorted({*subject_face_ids, *adjacent_face_ids})
|
||||
)
|
||||
role_groups = (
|
||||
{
|
||||
"role": "selected-same-domain-region",
|
||||
"face_ids": subject_face_ids,
|
||||
"count": len(subject_face_ids),
|
||||
},
|
||||
{
|
||||
"role": "direct-adjacent",
|
||||
"face_ids": adjacent_face_ids,
|
||||
"count": len(adjacent_face_ids),
|
||||
},
|
||||
)
|
||||
subject_role = "selected same-domain Face region"
|
||||
source_model = "face"
|
||||
|
||||
adjacent_surface_types = tuple(topology.get("first_level_adjacent_surface_types") or ()) or tuple(
|
||||
topology.get("cylindrical_feature_adjacent_surface_types") or ()
|
||||
)
|
||||
shared_edges = tuple(topology.get("first_level_shared_edges_by_face") or ()) or tuple(
|
||||
topology.get("cylindrical_feature_shared_edges_by_face") or ()
|
||||
)
|
||||
ignored_depths = tuple(topology.get("topology_ignored_relation_depths") or ("second-level", "third-level"))
|
||||
summary = (
|
||||
f"{subject_role}: Face {len(subject_face_ids)}, boundary Edge {len(boundary_edge_ids)}, "
|
||||
f"boundary Vertex {len(boundary_vertex_points)}, direct adjacent Face {len(adjacent_face_ids)}; "
|
||||
"deeper relations are recorded as future propagation targets, not edited automatically."
|
||||
)
|
||||
facts = {
|
||||
"first_level_fact_model": "STEP/B-Rep first-level fact graph",
|
||||
"first_level_fact_source_model": source_model,
|
||||
"first_level_fact_status": "ready",
|
||||
"first_level_fact_relation_depth": 1,
|
||||
"first_level_fact_relation_boundary": "shared-edge",
|
||||
"first_level_fact_scope": resolved_scope,
|
||||
"first_level_fact_subject_role": subject_role,
|
||||
"first_level_fact_subject_face_ids": tuple(sorted(set(subject_face_ids))),
|
||||
"first_level_fact_subject_face_count": len(set(subject_face_ids)),
|
||||
"first_level_fact_boundary_edge_ids": tuple(sorted(set(boundary_edge_ids))),
|
||||
"first_level_fact_boundary_edge_count": len(set(boundary_edge_ids)),
|
||||
"first_level_fact_boundary_vertex_points": boundary_vertex_points,
|
||||
"first_level_fact_boundary_vertex_count": len(boundary_vertex_points),
|
||||
"first_level_fact_adjacent_face_ids": tuple(sorted(set(adjacent_face_ids))),
|
||||
"first_level_fact_adjacent_face_count": len(set(adjacent_face_ids)),
|
||||
"first_level_fact_adjacent_surface_types": adjacent_surface_types,
|
||||
"first_level_fact_shared_edges_by_face": shared_edges,
|
||||
"first_level_fact_included_face_ids": tuple(sorted(set(included_face_ids))),
|
||||
"first_level_fact_included_face_count": len(set(included_face_ids)),
|
||||
"first_level_fact_role_groups": role_groups,
|
||||
"first_level_fact_ignored_relation_depths": ignored_depths,
|
||||
"first_level_fact_ignored_relation_note": topology.get("topology_ignored_relation_note", ""),
|
||||
"first_level_fact_summary": summary,
|
||||
}
|
||||
for item in facts["first_level_fact_subject_face_ids"]:
|
||||
self._face_first_level_fact_cache[(int(item), resolved_scope)] = dict(facts)
|
||||
self._face_first_level_fact_cache[cache_key] = dict(facts)
|
||||
return dict(facts)
|
||||
|
||||
def _face_boundary_wire_info(self, face: TopoDS_Shape) -> dict[str, object]:
|
||||
try:
|
||||
boundary_wires = len(_explore(face, TopAbs_WIRE))
|
||||
@@ -2109,6 +2513,23 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
|
||||
source_solid_id = self.face_solid_ids[face_id]
|
||||
tolerance = min(max(_shape_diagonal(self.shape) * 1e-7, 1e-6), 1e-3)
|
||||
visited = {face_id}
|
||||
queue = [face_id]
|
||||
while queue:
|
||||
current_id = queue.pop(0)
|
||||
for adjacent_id in self._adjacent_face_ids_for_edges(self._face_boundary_edge_ids(current_id), current_id):
|
||||
if adjacent_id in visited:
|
||||
continue
|
||||
if source_solid_id >= 0 and self.face_solid_ids[adjacent_id] != source_solid_id:
|
||||
continue
|
||||
candidate_surf = BRepAdaptor_Surface(self.faces[adjacent_id])
|
||||
if _surfaces_are_coplanar(source_surf, candidate_surf, tolerance):
|
||||
visited.add(adjacent_id)
|
||||
queue.append(adjacent_id)
|
||||
shared_edge_result = sorted(visited)
|
||||
if len(self.faces) > 600 or len(shared_edge_result) > 1:
|
||||
return shared_edge_result
|
||||
|
||||
interval_tolerance = max(tolerance * 20.0, _shape_diagonal(self.shape) * 1e-6, 1e-4)
|
||||
plane = source_surf.Plane()
|
||||
axis_point = plane.Location()
|
||||
@@ -2141,20 +2562,6 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
visited.add(candidate_id)
|
||||
queue.append(candidate_id)
|
||||
return sorted(visited)
|
||||
|
||||
visited = {face_id}
|
||||
queue = [face_id]
|
||||
while queue:
|
||||
current_id = queue.pop(0)
|
||||
for adjacent_id in self._adjacent_face_ids_for_edges(self._face_boundary_edge_ids(current_id), current_id):
|
||||
if adjacent_id in visited:
|
||||
continue
|
||||
if source_solid_id >= 0 and self.face_solid_ids[adjacent_id] != source_solid_id:
|
||||
continue
|
||||
candidate_surf = BRepAdaptor_Surface(self.faces[adjacent_id])
|
||||
if _surfaces_are_coplanar(source_surf, candidate_surf, tolerance):
|
||||
visited.add(adjacent_id)
|
||||
queue.append(adjacent_id)
|
||||
return sorted(visited)
|
||||
|
||||
def connected_same_domain_face_ids(self, face_id: int) -> list[int]:
|
||||
@@ -2189,9 +2596,25 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
radius = max(float(cylinder.Radius()), 0.0)
|
||||
diagonal = _shape_diagonal(self.shape)
|
||||
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
|
||||
visited = {face_id}
|
||||
queue = [face_id]
|
||||
while queue:
|
||||
current_id = queue.pop(0)
|
||||
for adjacent_id in self._adjacent_face_ids_for_edges(self._face_boundary_edge_ids(current_id), current_id):
|
||||
if adjacent_id in visited:
|
||||
continue
|
||||
if source_solid_id >= 0 and self.face_solid_ids[adjacent_id] != source_solid_id:
|
||||
continue
|
||||
candidate_surf = BRepAdaptor_Surface(self.faces[adjacent_id])
|
||||
if _surfaces_are_cocylindrical(source_surf, candidate_surf, tolerance):
|
||||
visited.add(adjacent_id)
|
||||
queue.append(adjacent_id)
|
||||
shared_edge_result = sorted(visited)
|
||||
if len(self.faces) > 600 or len(shared_edge_result) > 1:
|
||||
return shared_edge_result
|
||||
|
||||
interval_tolerance = max(tolerance * 50.0, diagonal * 1e-5, radius * 1e-4, 1e-3)
|
||||
source_interval = _shape_axis_interval(self.faces[face_id], axis_point, axis_dir)
|
||||
|
||||
candidates: dict[int, tuple[float, float]] = {}
|
||||
for candidate_id, face in enumerate(self.faces):
|
||||
if source_solid_id >= 0 and self.face_solid_ids[candidate_id] != source_solid_id:
|
||||
@@ -2220,20 +2643,6 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
visited.add(candidate_id)
|
||||
queue.append(candidate_id)
|
||||
return sorted(visited)
|
||||
|
||||
visited = {face_id}
|
||||
queue = [face_id]
|
||||
while queue:
|
||||
current_id = queue.pop(0)
|
||||
for adjacent_id in self._adjacent_face_ids_for_edges(self._face_boundary_edge_ids(current_id), current_id):
|
||||
if adjacent_id in visited:
|
||||
continue
|
||||
if source_solid_id >= 0 and self.face_solid_ids[adjacent_id] != source_solid_id:
|
||||
continue
|
||||
candidate_surf = BRepAdaptor_Surface(self.faces[adjacent_id])
|
||||
if _surfaces_are_cocylindrical(source_surf, candidate_surf, tolerance):
|
||||
visited.add(adjacent_id)
|
||||
queue.append(adjacent_id)
|
||||
return sorted(visited)
|
||||
|
||||
def _cylindrical_axis_range(
|
||||
|
||||
+2691
-138
File diff suppressed because it is too large
Load Diff
@@ -49,6 +49,25 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
|
||||
"curve",
|
||||
"adjacent_face_ids",
|
||||
"adjacent_face_count",
|
||||
"first_level_fact_model",
|
||||
"first_level_fact_status",
|
||||
"first_level_fact_relation_depth",
|
||||
"first_level_fact_relation_boundary",
|
||||
"first_level_fact_scope",
|
||||
"first_level_fact_source_model",
|
||||
"first_level_fact_subject_role",
|
||||
"first_level_fact_subject_face_ids",
|
||||
"first_level_fact_subject_face_count",
|
||||
"first_level_fact_boundary_edge_ids",
|
||||
"first_level_fact_boundary_edge_count",
|
||||
"first_level_fact_boundary_vertex_count",
|
||||
"first_level_fact_adjacent_face_ids",
|
||||
"first_level_fact_adjacent_face_count",
|
||||
"first_level_fact_included_face_ids",
|
||||
"first_level_fact_included_face_count",
|
||||
"first_level_fact_role_groups",
|
||||
"first_level_fact_ignored_relation_depths",
|
||||
"first_level_fact_summary",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -160,6 +179,7 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
|
||||
"push_pull_message",
|
||||
"push_pull_scope_face_ids",
|
||||
"push_pull_scope_face_count",
|
||||
"push_pull_scope_area",
|
||||
"push_pull_scope_note",
|
||||
"shell_region_kind",
|
||||
"shell_region_status",
|
||||
@@ -193,6 +213,18 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
|
||||
(
|
||||
"特征判断",
|
||||
[
|
||||
"recognition_summary",
|
||||
"recognition_candidate",
|
||||
"recognition_confidence",
|
||||
"recognition_risk",
|
||||
"recognition_score",
|
||||
"recognition_decision",
|
||||
"recognition_evidence",
|
||||
"recognition_evidence_keys",
|
||||
"recognition_ready_actions",
|
||||
"recognition_limited_actions",
|
||||
"recognition_blockers",
|
||||
"recognition_limitations",
|
||||
"feature_guess",
|
||||
"confidence",
|
||||
"material_vote_summary",
|
||||
@@ -372,6 +404,18 @@ INFO_LABELS = {
|
||||
"feature_mode": "特征模式说明",
|
||||
"feature_type": "特征类型",
|
||||
"feature_source_face_id": "特征来源 Face",
|
||||
"recognition_summary": "识别摘要",
|
||||
"recognition_candidate": "识别候选",
|
||||
"recognition_confidence": "识别置信度",
|
||||
"recognition_risk": "识别风险",
|
||||
"recognition_score": "识别评分",
|
||||
"recognition_decision": "识别结论",
|
||||
"recognition_evidence": "识别依据",
|
||||
"recognition_evidence_keys": "识别依据项",
|
||||
"recognition_ready_actions": "当前可改",
|
||||
"recognition_limited_actions": "当前受限修改",
|
||||
"recognition_blockers": "识别限制",
|
||||
"recognition_limitations": "受限能力",
|
||||
"parts": "零件数",
|
||||
"solids": "Solid 数",
|
||||
"faces": "Face 数",
|
||||
@@ -389,6 +433,25 @@ INFO_LABELS = {
|
||||
"curve": "曲线类型",
|
||||
"adjacent_face_ids": "相邻 Face",
|
||||
"adjacent_face_count": "相邻 Face 数",
|
||||
"first_level_fact_model": "一级事实模型",
|
||||
"first_level_fact_status": "一级事实状态",
|
||||
"first_level_fact_relation_depth": "一级事实深度",
|
||||
"first_level_fact_relation_boundary": "一级事实边界",
|
||||
"first_level_fact_scope": "一级事实范围",
|
||||
"first_level_fact_source_model": "一级事实来源",
|
||||
"first_level_fact_subject_role": "一级事实主体",
|
||||
"first_level_fact_subject_face_ids": "一级主体 Face",
|
||||
"first_level_fact_subject_face_count": "一级主体 Face 数",
|
||||
"first_level_fact_boundary_edge_ids": "一级边界 Edge",
|
||||
"first_level_fact_boundary_edge_count": "一级边界 Edge 数",
|
||||
"first_level_fact_boundary_vertex_count": "一级边界 Vertex 数",
|
||||
"first_level_fact_adjacent_face_ids": "一级相邻 Face",
|
||||
"first_level_fact_adjacent_face_count": "一级相邻 Face 数",
|
||||
"first_level_fact_included_face_ids": "一级范围 Face",
|
||||
"first_level_fact_included_face_count": "一级范围 Face 数",
|
||||
"first_level_fact_role_groups": "一级角色分组",
|
||||
"first_level_fact_ignored_relation_depths": "暂不传播层级",
|
||||
"first_level_fact_summary": "一级事实摘要",
|
||||
"volume": "体积",
|
||||
"surface_area": "表面积",
|
||||
"area": "面积",
|
||||
@@ -533,6 +596,7 @@ INFO_LABELS = {
|
||||
"push_pull_message": "推拉说明",
|
||||
"push_pull_scope_face_ids": "推拉共面区域 Face",
|
||||
"push_pull_scope_face_count": "推拉共面区域 Face 数",
|
||||
"push_pull_scope_area": "推拉共面区域面积",
|
||||
"push_pull_scope_note": "推拉共面区域说明",
|
||||
"shell_region_kind": "壳体/薄壁候选类型",
|
||||
"shell_region_status": "壳体/薄壁识别状态",
|
||||
@@ -905,8 +969,14 @@ def _export_quality_text(info: dict[str, object]) -> str:
|
||||
def _format_value(value: object) -> str:
|
||||
if isinstance(value, float):
|
||||
return _format_float(value)
|
||||
if isinstance(value, tuple):
|
||||
return "(" + ", ".join(_format_float(float(v)) for v in value) + ")"
|
||||
if isinstance(value, (tuple, list)):
|
||||
parts: list[str] = []
|
||||
for item in value:
|
||||
try:
|
||||
parts.append(_format_float(float(item)))
|
||||
except (TypeError, ValueError):
|
||||
parts.append(str(item))
|
||||
return "(" + ", ".join(parts) + ")"
|
||||
return str(value)
|
||||
|
||||
|
||||
|
||||
+637
-17
@@ -20,6 +20,15 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from .model import StepModel
|
||||
from .records import OperationRecord
|
||||
from .geometry_utils import (
|
||||
_tuple_add,
|
||||
_tuple_dot,
|
||||
_tuple_normalized,
|
||||
_tuple_or_none,
|
||||
_tuple_scale,
|
||||
_tuple_sub,
|
||||
_vector_length,
|
||||
)
|
||||
from .ui_helpers import * # noqa: F403
|
||||
from .workers import EditWorker, LoadWorker, ScanWorker
|
||||
|
||||
@@ -63,6 +72,11 @@ def _unit_triple_or_none(value: object) -> tuple[float, float, float] | None:
|
||||
return (triple[0] / length, triple[1] / length, triple[2] / length)
|
||||
|
||||
|
||||
def _compact_plan_value(value: object) -> str:
|
||||
text = _format_value(value)
|
||||
return text if len(text) <= 120 else text[:117] + "..."
|
||||
|
||||
|
||||
class WindowActionMixin:
|
||||
def export_all(self) -> None:
|
||||
if self.model is None:
|
||||
@@ -339,7 +353,7 @@ class WindowActionMixin:
|
||||
QMessageBox.critical(self, "距离无效", "请输入数字形式的面移动距离。")
|
||||
return
|
||||
face_id = self.selected_face_id
|
||||
plan = self._quick_push_pull_plan(face_id, distance)
|
||||
plan = self._push_pull_plan_for_action(face_id, distance)
|
||||
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能推拉平面", str(plan["message"]))
|
||||
@@ -411,6 +425,24 @@ class WindowActionMixin:
|
||||
"push_pull_inward_material_depth": plan.get("push_pull_inward_material_depth"),
|
||||
"push_pull_inward_cut_ratio": plan.get("push_pull_inward_cut_ratio"),
|
||||
"bbox_diagonal": plan.get("bbox_diagonal"),
|
||||
"selected_boundary_wires": plan.get("selected_boundary_wires"),
|
||||
"selected_inner_boundary_wires": plan.get("selected_inner_boundary_wires"),
|
||||
"selected_has_inner_boundaries": plan.get("selected_has_inner_boundaries"),
|
||||
"same_domain_face_count": plan.get("same_domain_face_count"),
|
||||
"first_level_boundary_edge_count": plan.get("first_level_boundary_edge_count"),
|
||||
"first_level_boundary_vertex_count": plan.get("first_level_boundary_vertex_count"),
|
||||
"first_level_adjacent_face_count": plan.get("first_level_adjacent_face_count"),
|
||||
"topology_relation_status": plan.get("topology_relation_status"),
|
||||
"topology_ignored_relation_note": plan.get("topology_ignored_relation_note"),
|
||||
"first_level_topology_note": plan.get("first_level_topology_note"),
|
||||
"planar_cap_extension_kind": plan.get("planar_cap_extension_kind"),
|
||||
"planar_cap_extension_method": plan.get("planar_cap_extension_method"),
|
||||
"planar_cap_boundary_edge_count": plan.get("planar_cap_boundary_edge_count"),
|
||||
"planar_cap_inner_boundary_wires": plan.get("planar_cap_inner_boundary_wires"),
|
||||
"planar_cap_adjacent_face_count": plan.get("planar_cap_adjacent_face_count"),
|
||||
"cylindrical_cap_extension_kind": plan.get("cylindrical_cap_extension_kind"),
|
||||
"cylindrical_cap_extension_method": plan.get("cylindrical_cap_extension_method"),
|
||||
"cap_extra_adjacent_face_count": plan.get("cap_extra_adjacent_face_count"),
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
@@ -495,6 +527,20 @@ class WindowActionMixin:
|
||||
if risk == "low":
|
||||
risk = "medium"
|
||||
warnings.append("向内推拉的材料厚度尚未缓存;完整切穿检查会放到后台计算。")
|
||||
model_face_count = len(self.model.faces) if self.model is not None else 0
|
||||
inner_wires = int(info.get("inner_boundary_wires", 0) or 0)
|
||||
if status != "blocked" and model_face_count > 600 and inner_wires > 1:
|
||||
status = "blocked"
|
||||
risk = "blocked"
|
||||
boundary_edges = _compact_plan_value(info.get("first_level_boundary_edge_count", "未知"))
|
||||
adjacent_faces = _compact_plan_value(info.get("first_level_adjacent_face_count", "未知"))
|
||||
blockers.append(
|
||||
f"当前 Face {face_id} 是复杂大模型里的多内孔/多边界平面端盖:"
|
||||
f"内边界 {inner_wires} 个,一级边界 Edge {boundary_edges} 条,"
|
||||
f"共享边一级相邻 Face {adjacent_faces} 个。向内推拉这类面需要判断孔壁、"
|
||||
"槽底或台阶背后的二级关系是否一起变化;当前阶段只自动处理一级关系。"
|
||||
"已在界面预检查阶段阻止,避免进入通用 OCCT 布尔后长时间卡住。"
|
||||
)
|
||||
if status != "blocked" and risk in {"medium", "high"}:
|
||||
status = "caution"
|
||||
|
||||
@@ -538,6 +584,10 @@ class WindowActionMixin:
|
||||
"surface": surface,
|
||||
"area": info.get("area"),
|
||||
"bbox_diagonal": bbox_diagonal,
|
||||
"boundary_wires": info.get("boundary_wires"),
|
||||
"inner_boundary_wires": info.get("inner_boundary_wires"),
|
||||
"has_inner_boundaries": bool(info.get("has_inner_boundaries")),
|
||||
"model_face_count": len(self.model.faces) if self.model is not None else None,
|
||||
"push_pull_inward_material_depth": inward_material_depth,
|
||||
"push_pull_inward_cut_ratio": inward_cut_ratio,
|
||||
"outward_direction": outward_tuple,
|
||||
@@ -554,8 +604,104 @@ class WindowActionMixin:
|
||||
"push_pull_scope_face_ids": tuple(scope_face_ids),
|
||||
"push_pull_scope_face_count": len(scope_face_ids),
|
||||
"push_pull_scope_note": "使用当前显示/选中缓存生成快速预览;后台会重新计算真实推拉区域。",
|
||||
"selected_boundary_wires": info.get("boundary_wires"),
|
||||
"selected_inner_boundary_wires": info.get("inner_boundary_wires"),
|
||||
"selected_has_inner_boundaries": bool(info.get("has_inner_boundaries")),
|
||||
"same_domain_face_count": info.get("same_domain_face_count"),
|
||||
"first_level_boundary_edge_count": info.get("first_level_boundary_edge_count"),
|
||||
"first_level_boundary_vertex_count": info.get("first_level_boundary_vertex_count"),
|
||||
"first_level_adjacent_face_count": info.get("first_level_adjacent_face_count"),
|
||||
"topology_relation_status": info.get("topology_relation_status"),
|
||||
"topology_ignored_relation_note": info.get("topology_ignored_relation_note"),
|
||||
"first_level_topology_note": info.get("first_level_topology_note"),
|
||||
"ui_quick_blocked_push_pull_plan": bool(
|
||||
status == "blocked"
|
||||
and distance < 0
|
||||
and (len(self.model.faces) if self.model is not None else 0) > 600
|
||||
and int(info.get("inner_boundary_wires", 0) or 0) > 1
|
||||
),
|
||||
}
|
||||
|
||||
def _should_defer_push_pull_model_plan(
|
||||
self,
|
||||
face_id: int,
|
||||
distance: float,
|
||||
quick_plan: dict[str, object],
|
||||
) -> bool:
|
||||
if self.model is None or quick_plan.get("status") == "blocked":
|
||||
return False
|
||||
if str(quick_plan.get("surface", "")) != "plane":
|
||||
return False
|
||||
try:
|
||||
model_face_count = len(self.model.faces)
|
||||
except Exception:
|
||||
model_face_count = 0
|
||||
if model_face_count < 600:
|
||||
return False
|
||||
|
||||
inner_wires = int(quick_plan.get("inner_boundary_wires") or 0)
|
||||
boundary_wires = int(quick_plan.get("boundary_wires") or 0)
|
||||
if inner_wires <= 0 and boundary_wires <= 1 and not bool(quick_plan.get("has_inner_boundaries")):
|
||||
return False
|
||||
|
||||
# Large STEP + holed planar caps are exactly where a full plan can spend
|
||||
# seconds scanning topology before the actual isolated edit even starts.
|
||||
return abs(float(distance)) > 1e-9
|
||||
|
||||
def _deferred_push_pull_model_plan(
|
||||
self,
|
||||
quick_plan: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
plan = dict(quick_plan)
|
||||
warnings = [item for item in str(plan.get("warnings") or "").split(";") if item]
|
||||
warnings.append(
|
||||
"当前是复杂大模型里的多边界平面;完整几何计划将放到后台/隔离子进程里计算,避免主界面先卡住。"
|
||||
)
|
||||
plan["warnings"] = ";".join(warnings)
|
||||
plan["risk"] = self._max_quick_risk(str(plan.get("risk") or "low"), "high")
|
||||
if plan.get("status") != "blocked":
|
||||
plan["status"] = "caution"
|
||||
plan["ui_deferred_model_plan"] = True
|
||||
plan["quick_plan_status"] = quick_plan.get("status")
|
||||
plan["quick_plan_risk"] = quick_plan.get("risk")
|
||||
plan["message"] = (
|
||||
"为避免复杂 STEP 在界面线程生成完整推拉计划时卡顿,本次只做快速预检查;"
|
||||
"真正的一级关系识别、风险判断、解析重建或快速阻止会在后台隔离进程中完成。"
|
||||
)
|
||||
plan.setdefault("edit_strategy_label", "后台计算完整推拉计划")
|
||||
plan.setdefault(
|
||||
"edit_semantics",
|
||||
"界面先提交后台任务;子进程会按当前 Face 的一级拓扑关系决定是解析重建、局部重建还是阻止。",
|
||||
)
|
||||
return plan
|
||||
|
||||
def _push_pull_plan_for_action(self, face_id: int, distance: float) -> dict[str, object]:
|
||||
quick_plan = self._quick_push_pull_plan(face_id, distance)
|
||||
if quick_plan.get("status") == "blocked":
|
||||
return quick_plan
|
||||
if self.model is None:
|
||||
return quick_plan
|
||||
if self._should_defer_push_pull_model_plan(face_id, distance, quick_plan):
|
||||
return self._deferred_push_pull_model_plan(quick_plan)
|
||||
try:
|
||||
model_plan = self.model.push_pull_plan(face_id, distance)
|
||||
except Exception as exc:
|
||||
blocked_plan = dict(quick_plan)
|
||||
blocked_plan.update(
|
||||
{
|
||||
"status": "blocked",
|
||||
"risk": "blocked",
|
||||
"message": f"无法生成完整推拉计划:{exc}",
|
||||
"model_plan_error": str(exc),
|
||||
"quick_plan_status": quick_plan.get("status"),
|
||||
"quick_plan_risk": quick_plan.get("risk"),
|
||||
}
|
||||
)
|
||||
return blocked_plan
|
||||
model_plan.setdefault("quick_plan_status", quick_plan.get("status"))
|
||||
model_plan.setdefault("quick_plan_risk", quick_plan.get("risk"))
|
||||
return model_plan
|
||||
|
||||
def resize_shell_thickness(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
@@ -573,7 +719,9 @@ class WindowActionMixin:
|
||||
return
|
||||
|
||||
face_id = self.selected_face_id
|
||||
plan = self.model.shell_thickness_plan(face_id, target_thickness)
|
||||
plan = self._deferred_shell_thickness_plan_if_needed(face_id, target_thickness)
|
||||
if plan is None:
|
||||
plan = self.model.shell_thickness_plan(face_id, target_thickness)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能调整薄壁厚度", str(plan["message"]))
|
||||
self.statusBar().showMessage("薄壁厚度调整已阻止")
|
||||
@@ -782,6 +930,11 @@ class WindowActionMixin:
|
||||
info = {}
|
||||
if info and str(info.get("kind", "")) in {"edge", "part", "solid"} and not has_selected_id:
|
||||
info = {}
|
||||
if "surface" not in info and "diameter" not in info:
|
||||
try:
|
||||
info = dict(self.model.quick_face_info(selected_face_id))
|
||||
except Exception:
|
||||
pass
|
||||
if "surface" not in info and "diameter" not in info:
|
||||
try:
|
||||
info = dict(self.model.face_info(selected_face_id))
|
||||
@@ -789,6 +942,286 @@ class WindowActionMixin:
|
||||
pass
|
||||
return info
|
||||
|
||||
def _large_complex_local_face_blocker(self, face_id: int, info: dict[str, object]) -> str:
|
||||
if self.model is None:
|
||||
return ""
|
||||
try:
|
||||
model_face_count = len(self.model.faces)
|
||||
except Exception:
|
||||
model_face_count = 0
|
||||
if model_face_count < 600:
|
||||
return ""
|
||||
if str(info.get("surface", "")) != "plane":
|
||||
return ""
|
||||
try:
|
||||
inner_wires = int(info.get("inner_boundary_wires") or 0)
|
||||
boundary_wires = int(info.get("boundary_wires") or 0)
|
||||
except (TypeError, ValueError):
|
||||
inner_wires = 0
|
||||
boundary_wires = 0
|
||||
local_ready = info.get("local_face_deform_ready")
|
||||
if inner_wires <= 0 and boundary_wires <= 1 and local_ready is not False:
|
||||
return ""
|
||||
base = str(info.get("local_face_deform_blocker") or "").strip()
|
||||
if not base:
|
||||
base = "当前 Face 位于复杂大 STEP 中,并且有内孔、内边界或复杂边界;不适合做只改当前 Face 的局部变形。"
|
||||
return (
|
||||
f"{base} 为避免在界面线程生成完整局部重建计划时卡顿,已在轻量预检查阶段阻止;"
|
||||
"请改用 `推拉当前面`、孔/槽专门入口、`移动整个特征` 或 `调整整个特征`。"
|
||||
)
|
||||
|
||||
def _quick_blocked_local_face_plan(
|
||||
self,
|
||||
face_id: int,
|
||||
blocker: str,
|
||||
*,
|
||||
resize_strategy: str,
|
||||
edit_strategy_label: str,
|
||||
edit_semantics: str,
|
||||
info: dict[str, object] | None = None,
|
||||
extra: dict[str, object] | None = None,
|
||||
) -> dict[str, object]:
|
||||
source = dict(info or self._selected_face_info_snapshot())
|
||||
plan = {
|
||||
"status": "blocked",
|
||||
"risk": "blocked",
|
||||
"message": blocker,
|
||||
"warnings": "",
|
||||
"blockers": blocker,
|
||||
"face_id": face_id,
|
||||
"part_id": source.get("part_id"),
|
||||
"solid_id": source.get("solid_id"),
|
||||
"surface": source.get("surface"),
|
||||
"area": source.get("area"),
|
||||
"area_center": source.get("area_center"),
|
||||
"bbox_diagonal": source.get("bbox_diagonal"),
|
||||
"boundary_wires": source.get("boundary_wires"),
|
||||
"inner_boundary_wires": source.get("inner_boundary_wires"),
|
||||
"has_inner_boundaries": bool(source.get("has_inner_boundaries")),
|
||||
"local_face_deform_ready": source.get("local_face_deform_ready"),
|
||||
"local_face_deform_blocker": source.get("local_face_deform_blocker"),
|
||||
"local_face_deform_face_count": source.get("local_face_deform_face_count", 1),
|
||||
"local_face_deform_moved_point_count": 0,
|
||||
"local_face_deform_target_kind": "blocked-quick-preflight",
|
||||
"resize_strategy": resize_strategy,
|
||||
"edit_strategy_label": edit_strategy_label,
|
||||
"edit_semantics": edit_semantics,
|
||||
"quick_preflight": True,
|
||||
"ui_quick_blocked_local_face_plan": True,
|
||||
}
|
||||
if extra:
|
||||
plan.update(extra)
|
||||
return plan
|
||||
|
||||
def _quick_blocked_face_area_local_plan(self, face_id: int, target_area: float) -> dict[str, object] | None:
|
||||
info = self._selected_face_info_snapshot()
|
||||
blocker = self._large_complex_local_face_blocker(face_id, info)
|
||||
if not blocker:
|
||||
return None
|
||||
current_area = _float_or_none(info.get("area"))
|
||||
area_delta = target_area - current_area if current_area is not None else None
|
||||
area_delta_ratio = abs(area_delta) / current_area if current_area is not None and current_area > 1e-9 else None
|
||||
area_scale = math.sqrt(target_area / current_area) if current_area is not None and current_area > 1e-9 and target_area > 0 else None
|
||||
return self._quick_blocked_local_face_plan(
|
||||
face_id,
|
||||
blocker,
|
||||
resize_strategy="local-face-area-only-deform",
|
||||
edit_strategy_label="只缩放当前Face面积",
|
||||
edit_semantics="只移动当前 Face 的边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
|
||||
info=info,
|
||||
extra={
|
||||
"current_area": current_area,
|
||||
"target_area": target_area,
|
||||
"area_delta": area_delta,
|
||||
"area_delta_ratio": area_delta_ratio,
|
||||
"local_face_area_scale": area_scale,
|
||||
},
|
||||
)
|
||||
|
||||
def _quick_blocked_face_size_local_plan(
|
||||
self,
|
||||
face_id: int,
|
||||
target_size: float,
|
||||
axis_key: str,
|
||||
) -> dict[str, object] | None:
|
||||
info = self._selected_face_info_snapshot()
|
||||
blocker = self._large_complex_local_face_blocker(face_id, info)
|
||||
if not blocker:
|
||||
return None
|
||||
current_width = _float_or_none(info.get("local_face_width"))
|
||||
current_height = _float_or_none(info.get("local_face_height"))
|
||||
current_size = current_height if axis_key == "height" else current_width
|
||||
delta = target_size - current_size if current_size is not None else None
|
||||
ratio = abs(delta) / current_size if current_size is not None and current_size > 1e-9 else None
|
||||
scale = target_size / current_size if current_size is not None and current_size > 1e-9 else None
|
||||
axis_label = "面高" if axis_key == "height" else "面宽"
|
||||
return self._quick_blocked_local_face_plan(
|
||||
face_id,
|
||||
blocker,
|
||||
resize_strategy=f"local-face-{axis_key}-only-deform",
|
||||
edit_strategy_label=f"{axis_label}(当前面)",
|
||||
edit_semantics="只沿当前 Face 的一个面内方向移动边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
|
||||
info=info,
|
||||
extra={
|
||||
"face_size_axis": axis_key,
|
||||
"face_size_label": axis_label,
|
||||
"current_face_width": current_width,
|
||||
"target_face_width": target_size if axis_key == "width" else current_width,
|
||||
"current_face_height": current_height,
|
||||
"target_face_height": target_size if axis_key == "height" else current_height,
|
||||
"current_face_size": current_size,
|
||||
"target_face_size": target_size,
|
||||
"face_size_delta": delta,
|
||||
"face_size_delta_ratio": ratio,
|
||||
"face_size_scale": scale,
|
||||
"face_size_center": info.get("area_center") or info.get("bbox_center"),
|
||||
"face_size_axis_direction": info.get("face_height_direction" if axis_key == "height" else "face_width_direction"),
|
||||
"face_width_direction": info.get("face_width_direction"),
|
||||
"face_height_direction": info.get("face_height_direction"),
|
||||
},
|
||||
)
|
||||
|
||||
def _quick_blocked_face_center_local_plan(
|
||||
self,
|
||||
face_id: int,
|
||||
target_center: tuple[float, float, float],
|
||||
) -> dict[str, object] | None:
|
||||
info = self._selected_face_info_snapshot()
|
||||
blocker = self._large_complex_local_face_blocker(face_id, info)
|
||||
if not blocker:
|
||||
return None
|
||||
current_center = _tuple_or_none(info.get("area_center")) or _tuple_or_none(info.get("bbox_center"))
|
||||
move_vector = _tuple_sub(target_center, current_center) if current_center is not None else None
|
||||
move_distance = _vector_length(move_vector) if move_vector is not None else None
|
||||
bbox_diagonal = _float_or_none(info.get("bbox_diagonal"))
|
||||
move_ratio = move_distance / bbox_diagonal if move_distance is not None and bbox_diagonal is not None and bbox_diagonal > 1e-9 else None
|
||||
return self._quick_blocked_local_face_plan(
|
||||
face_id,
|
||||
blocker,
|
||||
resize_strategy="local-face-only-deform",
|
||||
edit_strategy_label="只移动当前Face",
|
||||
edit_semantics="只移动当前 Face 的边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
|
||||
info=info,
|
||||
extra={
|
||||
"current_face_center": current_center,
|
||||
"target_face_center": target_center,
|
||||
"face_center_move_vector": move_vector,
|
||||
"face_center_move_distance": move_distance,
|
||||
"face_center_move_ratio": move_ratio,
|
||||
},
|
||||
)
|
||||
|
||||
def _quick_blocked_face_plane_offset_local_plan(self, face_id: int, distance: float) -> dict[str, object] | None:
|
||||
info = self._selected_face_info_snapshot()
|
||||
blocker = self._large_complex_local_face_blocker(face_id, info)
|
||||
if not blocker:
|
||||
return None
|
||||
plane_origin = _tuple_or_none(info.get("plane_origin"))
|
||||
plane_direction = (
|
||||
_tuple_normalized(_tuple_or_none(info.get("push_pull_outward_direction")))
|
||||
or _tuple_normalized(_tuple_or_none(info.get("oriented_normal")))
|
||||
or _tuple_normalized(_tuple_or_none(info.get("normal")))
|
||||
)
|
||||
current_position = _tuple_dot(plane_origin, plane_direction) if plane_origin is not None and plane_direction is not None else None
|
||||
target_position = current_position + distance if current_position is not None else None
|
||||
current_center = _tuple_or_none(info.get("area_center")) or _tuple_or_none(info.get("bbox_center"))
|
||||
move_vector = _tuple_scale(plane_direction, distance) if plane_direction is not None else None
|
||||
target_center = _tuple_add(current_center, move_vector) if current_center is not None and move_vector is not None else None
|
||||
move_distance = abs(float(distance))
|
||||
bbox_diagonal = _float_or_none(info.get("bbox_diagonal"))
|
||||
move_ratio = move_distance / bbox_diagonal if bbox_diagonal is not None and bbox_diagonal > 1e-9 else None
|
||||
return self._quick_blocked_local_face_plan(
|
||||
face_id,
|
||||
blocker,
|
||||
resize_strategy="local-face-plane-offset-deform",
|
||||
edit_strategy_label="面偏移(当前面)",
|
||||
edit_semantics="按当前面垂直方向移动 Face 边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
|
||||
info=info,
|
||||
extra={
|
||||
"current_plane_position": current_position,
|
||||
"target_plane_position": target_position,
|
||||
"plane_origin": plane_origin,
|
||||
"plane_direction": plane_direction,
|
||||
"plane_offset_distance": distance,
|
||||
"current_face_center": current_center,
|
||||
"target_face_center": target_center,
|
||||
"face_center_move_vector": move_vector,
|
||||
"face_center_move_distance": move_distance,
|
||||
"face_center_move_ratio": move_ratio,
|
||||
},
|
||||
)
|
||||
|
||||
def _deferred_shell_thickness_plan_if_needed(
|
||||
self,
|
||||
face_id: int,
|
||||
target_thickness: float,
|
||||
) -> dict[str, object] | None:
|
||||
info = self._selected_face_info_snapshot()
|
||||
blocker = self._large_complex_local_face_blocker(face_id, info)
|
||||
if not blocker:
|
||||
return None
|
||||
current = (
|
||||
_float_or_none(info.get("shell_thickness_estimate"))
|
||||
or _float_or_none(info.get("shell_current_thickness"))
|
||||
or _float_or_none(info.get("shell_signed_thickness"))
|
||||
)
|
||||
if target_thickness <= 0:
|
||||
return self._quick_blocked_local_face_plan(
|
||||
face_id,
|
||||
"目标薄壁厚度必须大于 0。",
|
||||
resize_strategy="push-pull-shell-source-plane-to-target-thickness",
|
||||
edit_strategy_label="薄壁厚度(当前面)",
|
||||
edit_semantics="移动当前平面区域以接近目标薄壁厚度。",
|
||||
info=info,
|
||||
extra={
|
||||
"shell_current_thickness": current,
|
||||
"shell_target_thickness": target_thickness,
|
||||
"shell_delta_thickness": None,
|
||||
"shell_delta_ratio": None,
|
||||
},
|
||||
)
|
||||
delta = target_thickness - current if current is not None else None
|
||||
ratio = abs(delta) / current if current is not None and current > 1e-9 else None
|
||||
warnings = [
|
||||
"复杂大 STEP 的薄壁相对面识别会放到后台隔离进程里执行,避免主界面先卡住。",
|
||||
"执行前请确认这是想修改的局部薄壁区域,而不是孔/槽或台阶端面。",
|
||||
]
|
||||
return {
|
||||
"status": "caution",
|
||||
"risk": "high",
|
||||
"message": (
|
||||
"为避免在界面线程扫描复杂模型里的相对面,本次只做快速预检查;"
|
||||
"真正的薄壁相对面识别、推拉距离计算和结果校验会在后台隔离进程中完成。"
|
||||
),
|
||||
"warnings": ";".join(warnings),
|
||||
"blockers": "",
|
||||
"face_id": face_id,
|
||||
"part_id": info.get("part_id"),
|
||||
"solid_id": info.get("solid_id"),
|
||||
"surface": info.get("surface"),
|
||||
"shell_current_thickness": current,
|
||||
"shell_target_thickness": target_thickness,
|
||||
"shell_delta_thickness": delta,
|
||||
"shell_delta_ratio": ratio,
|
||||
"shell_signed_thickness": info.get("shell_signed_thickness"),
|
||||
"shell_opposite_face_id": info.get("shell_opposite_face_id"),
|
||||
"shell_overlap_ratio_estimate": info.get("shell_overlap_ratio_estimate"),
|
||||
"shell_confidence": info.get("shell_confidence", "deferred"),
|
||||
"shell_source_face_ids": info.get("shell_source_face_ids", (face_id,)),
|
||||
"shell_region_kind": info.get("shell_region_kind", "deferred-complex-face"),
|
||||
"push_pull_distance": None,
|
||||
"outward_direction": info.get("push_pull_outward_direction") or info.get("oriented_normal") or info.get("normal"),
|
||||
"push_pull_scope_face_ids": info.get("push_pull_scope_face_ids") or (face_id,),
|
||||
"push_pull_scope_face_count": len(_int_values(info.get("push_pull_scope_face_ids")) or [face_id]),
|
||||
"push_pull_scope_note": "复杂薄壁计划延后到后台隔离进程重新计算。",
|
||||
"resize_strategy": "push-pull-shell-source-plane-to-target-thickness",
|
||||
"edit_strategy_label": "薄壁厚度(当前面)",
|
||||
"edit_semantics": "后台识别相对平面后,把当前平面区域推拉到目标薄壁厚度;失败会保持原模型不变。",
|
||||
"quick_preflight": True,
|
||||
"ui_deferred_model_plan": True,
|
||||
"ui_deferred_shell_thickness_plan": True,
|
||||
}
|
||||
|
||||
def _quick_cylinder_resize_plan(
|
||||
self,
|
||||
face_id: int,
|
||||
@@ -982,6 +1415,8 @@ class WindowActionMixin:
|
||||
"面高(整体)",
|
||||
"面偏移(当前面)",
|
||||
"只移动当前Face",
|
||||
"圆柱高度调整",
|
||||
"高度(整体)缩放所属对象",
|
||||
}
|
||||
|
||||
def _isolation_for_plan(
|
||||
@@ -1003,6 +1438,9 @@ class WindowActionMixin:
|
||||
"move_face_center_local",
|
||||
"resize_shell_thickness",
|
||||
"resize_shell_thickness_owning_scale",
|
||||
"resize_cylindrical_height",
|
||||
"resize_cylindrical_boss_height",
|
||||
"resize_cylindrical_height_owning_scale",
|
||||
"resize_cone_reference_radius",
|
||||
"resize_cone_semi_angle",
|
||||
"resize_sphere_radius",
|
||||
@@ -1019,6 +1457,108 @@ class WindowActionMixin:
|
||||
"reason": f"{risk}-risk-face-occ-edit",
|
||||
}
|
||||
|
||||
def _edit_failure_diagnostics(self, context: dict[str, object]) -> str:
|
||||
parameters = context.get("parameters")
|
||||
if not isinstance(parameters, dict):
|
||||
parameters = {}
|
||||
lines: list[str] = []
|
||||
operation_name = str(context.get("operation_name") or "").strip()
|
||||
target = str(context.get("target") or "").strip()
|
||||
if operation_name or target:
|
||||
lines.append(f"操作: {operation_name or '未知'} / {target or '未知对象'}")
|
||||
|
||||
strategy = str(parameters.get("edit_strategy_label") or "").strip()
|
||||
if strategy:
|
||||
lines.append(f"编辑策略: {strategy}")
|
||||
|
||||
surface = parameters.get("surface")
|
||||
distance = parameters.get("semantic_distance")
|
||||
current_position = parameters.get("current_plane_position")
|
||||
target_position = parameters.get("target_plane_position")
|
||||
surface_chunks: list[str] = []
|
||||
if surface not in {None, ""}:
|
||||
surface_chunks.append(f"面类型={_compact_plan_value(surface)}")
|
||||
if distance not in {None, ""}:
|
||||
surface_chunks.append(f"修改量={_compact_plan_value(distance)}")
|
||||
if current_position not in {None, ""}:
|
||||
surface_chunks.append(f"当前位置={_compact_plan_value(current_position)}")
|
||||
if target_position not in {None, ""}:
|
||||
surface_chunks.append(f"目标位置={_compact_plan_value(target_position)}")
|
||||
if surface_chunks:
|
||||
lines.append("当前对象: " + ",".join(surface_chunks))
|
||||
|
||||
boundary_chunks: list[str] = []
|
||||
boundary_keys = (
|
||||
("线圈", "selected_boundary_wires"),
|
||||
("内孔/内边界", "selected_inner_boundary_wires"),
|
||||
("同域Face", "same_domain_face_count"),
|
||||
("一级Edge", "first_level_boundary_edge_count"),
|
||||
("一级Vertex", "first_level_boundary_vertex_count"),
|
||||
("一级相邻Face", "first_level_adjacent_face_count"),
|
||||
)
|
||||
for label, key in boundary_keys:
|
||||
value = parameters.get(key)
|
||||
if value not in {None, ""}:
|
||||
boundary_chunks.append(f"{label}={_compact_plan_value(value)}")
|
||||
if boundary_chunks:
|
||||
lines.append("一级关系证据: " + ",".join(boundary_chunks))
|
||||
|
||||
topology_note = str(parameters.get("first_level_topology_note") or "").strip()
|
||||
ignored_note = str(parameters.get("topology_ignored_relation_note") or "").strip()
|
||||
if topology_note:
|
||||
lines.append(f"一级关系说明: {topology_note}")
|
||||
if ignored_note:
|
||||
lines.append(f"暂不处理范围: {ignored_note}")
|
||||
|
||||
planar_method = str(parameters.get("planar_cap_extension_method") or "").strip()
|
||||
cylindrical_method = str(parameters.get("cylindrical_cap_extension_method") or "").strip()
|
||||
if planar_method or cylindrical_method:
|
||||
method_parts: list[str] = []
|
||||
if planar_method:
|
||||
method_parts.append(
|
||||
f"平面端盖={_compact_plan_value(parameters.get('planar_cap_extension_kind'))}"
|
||||
f"/{_compact_plan_value(planar_method)}"
|
||||
)
|
||||
if cylindrical_method:
|
||||
method_parts.append(
|
||||
f"圆柱端盖={_compact_plan_value(parameters.get('cylindrical_cap_extension_kind'))}"
|
||||
f"/{_compact_plan_value(cylindrical_method)}"
|
||||
)
|
||||
lines.append("已识别的专用路径: " + ",".join(method_parts))
|
||||
|
||||
risk_message = str(parameters.get("push_pull_message") or "").strip()
|
||||
if risk_message:
|
||||
lines.append(f"计划阶段判断: {risk_message}")
|
||||
|
||||
isolation = context.get("isolation")
|
||||
if isinstance(isolation, dict) and isolation:
|
||||
timeout = isolation.get("timeout_seconds")
|
||||
reason = str(isolation.get("reason") or "").strip()
|
||||
if timeout not in {None, ""}:
|
||||
lines.append(f"隔离保护: 子进程超时上限 {_compact_plan_value(timeout)}s")
|
||||
if reason:
|
||||
lines.append(f"隔离原因: {reason}")
|
||||
|
||||
has_inner_boundaries = bool(parameters.get("selected_has_inner_boundaries"))
|
||||
try:
|
||||
inner_wires = int(parameters.get("selected_inner_boundary_wires", 0) or 0)
|
||||
except (TypeError, ValueError):
|
||||
inner_wires = 0
|
||||
if has_inner_boundaries or inner_wires > 0:
|
||||
lines.append(
|
||||
"可能原因: 当前面带孔/内边界。若修改方向会越过孔壁、槽底、台阶终点,"
|
||||
"就可能涉及一级相邻面背后的二级或更深拓扑关系;当前阶段只自动传播一级关系。"
|
||||
)
|
||||
elif str(parameters.get("push_pull_risk") or "") == "high":
|
||||
lines.append(
|
||||
"可能原因: 当前操作风险较高,底层 OCCT 布尔或局部重建可能返回无效 B-Rep;"
|
||||
"这通常不是界面卡住,而是几何内核没有稳定给出可用结果。"
|
||||
)
|
||||
|
||||
if not lines:
|
||||
return ""
|
||||
return "\n\n诊断信息:\n" + "\n".join(f"- {line}" for line in lines)
|
||||
|
||||
def resize_hole(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
@@ -2413,7 +2953,7 @@ class WindowActionMixin:
|
||||
self.statusBar().showMessage("已取消圆柱凸台高度调整")
|
||||
return
|
||||
|
||||
self._show_cylinder_boss_height_preview(face_id, target_height)
|
||||
self.clear_edit_preview(render=False)
|
||||
|
||||
def action():
|
||||
return self.model.resize_cylindrical_boss_height(face_id, target_height)
|
||||
@@ -2451,6 +2991,11 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=self._isolation_for_plan(
|
||||
plan,
|
||||
"resize_cylindrical_boss_height",
|
||||
[face_id, target_height],
|
||||
),
|
||||
)
|
||||
|
||||
def resize_cylinder_height(self) -> None:
|
||||
@@ -2527,6 +3072,7 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=self._isolation_for_plan(plan, "resize_cylindrical_height", [face_id, target_height]),
|
||||
)
|
||||
|
||||
def resize_cylindrical_height_owning_scale(self) -> None:
|
||||
@@ -2616,6 +3162,11 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=self._isolation_for_plan(
|
||||
plan,
|
||||
"resize_cylindrical_height_owning_scale",
|
||||
[face_id, target_height],
|
||||
),
|
||||
)
|
||||
|
||||
def suppress_hole(self) -> None:
|
||||
@@ -4659,7 +5210,9 @@ class WindowActionMixin:
|
||||
return
|
||||
|
||||
face_id = self.selected_face_id
|
||||
plan = self.model.face_area_local_resize_plan(face_id, target_area)
|
||||
plan = self._quick_blocked_face_area_local_plan(face_id, target_area)
|
||||
if plan is None:
|
||||
plan = self.model.face_area_local_resize_plan(face_id, target_area)
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
f"当前面积: {_format_value(plan.get('current_area'))}",
|
||||
@@ -4750,7 +5303,9 @@ class WindowActionMixin:
|
||||
return
|
||||
|
||||
face_id = self.selected_face_id
|
||||
plan = self.model.face_size_local_resize_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:
|
||||
plan = self.model.face_size_local_resize_plan(face_id, target_size, axis_key)
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
f"修改对象: {axis_label}(当前面)",
|
||||
@@ -5010,7 +5565,9 @@ class WindowActionMixin:
|
||||
return
|
||||
|
||||
face_id = self.selected_face_id
|
||||
plan = self.model.face_plane_offset_local_plan(face_id, distance)
|
||||
plan = self._quick_blocked_face_plane_offset_local_plan(face_id, distance)
|
||||
if plan is None:
|
||||
plan = self.model.face_plane_offset_local_plan(face_id, distance)
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
f"当前面偏移: {_format_value(plan.get('current_plane_position'))}",
|
||||
@@ -5177,7 +5734,9 @@ class WindowActionMixin:
|
||||
current_center[1] + vector[1],
|
||||
current_center[2] + vector[2],
|
||||
)
|
||||
plan = self.model.face_center_local_move_plan(face_id, target_center)
|
||||
plan = self._quick_blocked_face_center_local_plan(face_id, target_center)
|
||||
if plan is None:
|
||||
plan = self.model.face_center_local_move_plan(face_id, target_center)
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
f"当前Face中心: {_format_value(plan.get('current_face_center'))}",
|
||||
@@ -6450,7 +7009,10 @@ class WindowActionMixin:
|
||||
raise RuntimeError(
|
||||
f"编辑失败,且回滚到操作前状态也失败:{rollback_exc}\n原始错误:{exc}"
|
||||
) from exc
|
||||
raise RuntimeError(f"编辑失败,模型已恢复到操作前状态:{exc}") from exc
|
||||
raise RuntimeError(
|
||||
f"编辑失败,模型已恢复到操作前状态:{exc}"
|
||||
f"{self._edit_failure_diagnostics(context)}"
|
||||
) from exc
|
||||
model_polydata = None
|
||||
edge_polydata = None
|
||||
try:
|
||||
@@ -6522,22 +7084,38 @@ class WindowActionMixin:
|
||||
)
|
||||
|
||||
command = self._isolated_edit_command(request_path)
|
||||
self.isolated_edit_cancel_requested = False
|
||||
process: subprocess.Popen[str] | None = None
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
cwd=project_root,
|
||||
capture_output=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=timeout_seconds,
|
||||
check=False,
|
||||
)
|
||||
self.active_isolated_edit_process = process
|
||||
stdout, stderr = process.communicate(timeout=timeout_seconds)
|
||||
completed = subprocess.CompletedProcess(command, process.returncode, stdout, stderr)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
self._terminate_isolated_edit_process(process)
|
||||
try:
|
||||
stdout, stderr = process.communicate(timeout=5.0) if process is not None else ("", "")
|
||||
except Exception:
|
||||
stdout, stderr = "", ""
|
||||
raise RuntimeError(
|
||||
f"隔离子进程执行超时,已终止危险计算;主程序和原模型保持不变。"
|
||||
f" 超时时间: {timeout_seconds:g}s"
|
||||
f"{self._edit_failure_diagnostics(context)}"
|
||||
) from exc
|
||||
finally:
|
||||
if getattr(self, "active_isolated_edit_process", None) is process:
|
||||
self.active_isolated_edit_process = None
|
||||
|
||||
if getattr(self, "isolated_edit_cancel_requested", False):
|
||||
raise RuntimeError("隔离子进程已取消;主程序和原模型保持不变。")
|
||||
|
||||
response_path = request_path.with_suffix(".response.json")
|
||||
response: dict[str, object] = {}
|
||||
@@ -6551,6 +7129,7 @@ class WindowActionMixin:
|
||||
raise RuntimeError(
|
||||
"隔离子进程执行失败;主程序没有崩溃,原模型保持不变。"
|
||||
f" 子进程返回码: {completed.returncode}. 错误: {error}"
|
||||
f"{self._edit_failure_diagnostics(context)}"
|
||||
)
|
||||
if not output_path.exists():
|
||||
raise RuntimeError("隔离子进程报告成功,但没有生成结果 STEP;原模型保持不变。")
|
||||
@@ -6576,14 +7155,13 @@ class WindowActionMixin:
|
||||
after_quality,
|
||||
after_model=new_model,
|
||||
)
|
||||
self.model = new_model
|
||||
after_geometry: dict[str, object] = {}
|
||||
model_polydata = None
|
||||
edge_polydata = None
|
||||
try:
|
||||
deflection = float(context.get("edit_result_deflection", 1.6))
|
||||
model_polydata = self.model.build_face_polydata(deflection=deflection)
|
||||
edge_polydata = self.model.build_edge_polydata(
|
||||
model_polydata = new_model.build_face_polydata(deflection=deflection)
|
||||
edge_polydata = new_model.build_edge_polydata(
|
||||
deflection=deflection,
|
||||
show_same_domain_internal_edges=bool(context.get("show_same_domain_internal_edges", False)),
|
||||
)
|
||||
@@ -6600,6 +7178,7 @@ class WindowActionMixin:
|
||||
"after_snapshot": after_snapshot,
|
||||
"after_stats": after_stats,
|
||||
"after_part_stats": after_part_stats,
|
||||
"after_model": new_model,
|
||||
"quality_warnings": quality_warnings,
|
||||
"after_geometry": after_geometry,
|
||||
"model_polydata": model_polydata,
|
||||
@@ -6611,6 +7190,35 @@ class WindowActionMixin:
|
||||
return [sys.executable, "--isolated-edit-worker", str(request_path)]
|
||||
return [sys.executable, "-m", "step_editor.isolated_edit_worker", str(request_path)]
|
||||
|
||||
def _terminate_isolated_edit_process(self, process: subprocess.Popen | None = None) -> bool:
|
||||
target = process or getattr(self, "active_isolated_edit_process", None)
|
||||
if target is None:
|
||||
return False
|
||||
try:
|
||||
if target.poll() is not None:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
self.isolated_edit_cancel_requested = True
|
||||
try:
|
||||
target.terminate()
|
||||
return True
|
||||
except Exception:
|
||||
try:
|
||||
target.kill()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _cancel_active_edit_for_close(self) -> bool:
|
||||
if not (self.operation_in_progress or (self.edit_thread is not None and self.edit_thread.isRunning())):
|
||||
return False
|
||||
if not self._terminate_isolated_edit_process():
|
||||
return False
|
||||
self.close_after_edit_cancel = True
|
||||
self.statusBar().showMessage("正在取消后台几何计算,子进程已请求终止...")
|
||||
return True
|
||||
|
||||
def _preserve_isolated_face_logical_id(
|
||||
self,
|
||||
model: StepModel,
|
||||
@@ -7285,6 +7893,9 @@ class WindowActionMixin:
|
||||
QMessageBox.critical(self, "操作失败", "后台编辑返回了无法识别的结果。")
|
||||
self.statusBar().showMessage("编辑结果无法识别")
|
||||
return
|
||||
after_model = result.get("after_model")
|
||||
if after_model is not None:
|
||||
self.model = after_model
|
||||
message = str(result["message"])
|
||||
try:
|
||||
record = self._make_operation_record(
|
||||
@@ -7347,11 +7958,20 @@ class WindowActionMixin:
|
||||
if hasattr(self, "_is_ui_thread") and not self._is_ui_thread():
|
||||
self._invoke_on_ui_thread(lambda message=message: self._fail_edit_action(message))
|
||||
return
|
||||
close_after_cancel = bool(getattr(self, "close_after_edit_cancel", False))
|
||||
if close_after_cancel:
|
||||
self.close_after_edit_cancel = False
|
||||
self._end_edit_task(clear_preview=True)
|
||||
QMessageBox.critical(self, "操作失败", message)
|
||||
if not close_after_cancel:
|
||||
QMessageBox.critical(self, "操作失败", message)
|
||||
self._clear_editable_candidates()
|
||||
self._clear_cylinder_candidates()
|
||||
self.statusBar().showMessage("操作失败,模型已保持在编辑前状态")
|
||||
self.statusBar().showMessage("后台编辑已取消,模型已保持在编辑前状态" if close_after_cancel else "操作失败,模型已保持在编辑前状态")
|
||||
if close_after_cancel:
|
||||
if self.edit_thread is not None and self.edit_thread.isRunning():
|
||||
self.edit_thread.quit()
|
||||
self.edit_thread.wait(1500)
|
||||
self.close()
|
||||
|
||||
def _restore_failed_edit_snapshot(self, snapshot: object) -> str:
|
||||
if self.model is None or not isinstance(snapshot, dict):
|
||||
|
||||
@@ -214,6 +214,9 @@ class WindowCoreMixin:
|
||||
return
|
||||
edit_thread_running = bool(self.edit_thread is not None and self.edit_thread.isRunning())
|
||||
if self.operation_in_progress or edit_thread_running:
|
||||
if hasattr(self, "_cancel_active_edit_for_close") and self._cancel_active_edit_for_close():
|
||||
event.ignore()
|
||||
return
|
||||
self.statusBar().showMessage("后台编辑正在计算,请等待当前操作完成后再关闭窗口。")
|
||||
event.ignore()
|
||||
return
|
||||
|
||||
+236
-33
@@ -30,6 +30,7 @@ PROPERTY_ACTION_COLUMN = 4
|
||||
|
||||
FEATURE_EDIT_SEMANTICS_KEYS = {
|
||||
"face_first_level_topology",
|
||||
"cylindrical_feature_first_level_topology",
|
||||
"slot_edit_semantics",
|
||||
"hole_edit_semantics",
|
||||
"boss_edit_semantics",
|
||||
@@ -160,6 +161,26 @@ class WindowStateMixin:
|
||||
enriched["pick_position"] = pick_position
|
||||
return enriched
|
||||
|
||||
def _first_level_fact_selection_fields(self, face_id: int, scope: str = "auto") -> dict[str, object]:
|
||||
if self.model is None:
|
||||
return {}
|
||||
try:
|
||||
return self.model.face_first_level_facts(face_id, scope=scope)
|
||||
except Exception as exc:
|
||||
return {
|
||||
"first_level_fact_model": "STEP/B-Rep first-level fact graph",
|
||||
"first_level_fact_status": "unavailable",
|
||||
"first_level_fact_relation_depth": 1,
|
||||
"first_level_fact_scope": scope,
|
||||
"first_level_fact_subject_face_ids": (face_id,),
|
||||
"first_level_fact_subject_face_count": 1,
|
||||
"first_level_fact_boundary_edge_count": 0,
|
||||
"first_level_fact_boundary_vertex_count": 0,
|
||||
"first_level_fact_adjacent_face_count": 0,
|
||||
"first_level_fact_ignored_relation_depths": ("second-level", "third-level", "deeper"),
|
||||
"first_level_fact_summary": f"当前 Face 的一级事实图暂时无法生成:{exc}",
|
||||
}
|
||||
|
||||
def _face_first_level_selection_fields(self, face_id: int) -> dict[str, object]:
|
||||
if self.model is None:
|
||||
return {}
|
||||
@@ -198,6 +219,70 @@ class WindowStateMixin:
|
||||
"first_level_face_ids": topology.get("first_level_face_ids", (face_id,)),
|
||||
"first_level_face_count": topology.get("first_level_face_count", 1),
|
||||
"first_level_topology_note": topology.get("first_level_topology_note", ""),
|
||||
**self._first_level_fact_selection_fields(face_id, scope="face"),
|
||||
}
|
||||
|
||||
def _cylindrical_first_level_selection_fields(self, face_id: int) -> dict[str, object]:
|
||||
if self.model is None:
|
||||
return {}
|
||||
try:
|
||||
topology = self.model.cylindrical_feature_first_level_topology(face_id)
|
||||
except Exception as exc:
|
||||
return {
|
||||
"topology_relation_depth": 1,
|
||||
"topology_relation_model": "STEP/B-Rep cylindrical-feature shared-edge first-level",
|
||||
"topology_relation_status": "unavailable",
|
||||
"topology_relation_message": str(exc),
|
||||
"topology_ignored_relation_depths": ("second-level", "third-level", "deeper"),
|
||||
"topology_ignored_relation_note": "当前阶段只传播一级关系;二级、三级拓扑暂不自动递归编辑。",
|
||||
"cylindrical_feature_side_face_count": 1,
|
||||
"cylindrical_feature_boundary_edge_count": 0,
|
||||
"cylindrical_feature_boundary_vertex_count": 0,
|
||||
"cylindrical_feature_adjacent_face_count": 0,
|
||||
"cylindrical_feature_end_face_count": 0,
|
||||
"cylindrical_feature_bottom_face_count": 0,
|
||||
"cylindrical_feature_opening_face_count": 0,
|
||||
"cylindrical_feature_slot_boundary_face_count": 0,
|
||||
"first_level_topology_note": f"当前圆柱特征的一级关系暂时无法确认:{exc}",
|
||||
}
|
||||
return {
|
||||
"topology_relation_depth": topology.get("topology_relation_depth", 1),
|
||||
"topology_relation_model": topology.get("topology_relation_model"),
|
||||
"topology_relation_scope": topology.get("topology_relation_scope"),
|
||||
"topology_relation_boundary": topology.get("topology_relation_boundary"),
|
||||
"topology_relation_status": "ready",
|
||||
"topology_ignored_relation_depths": topology.get("topology_ignored_relation_depths", ()),
|
||||
"topology_ignored_relation_note": topology.get("topology_ignored_relation_note", ""),
|
||||
"first_level_boundary_edge_ids": topology.get("cylindrical_feature_boundary_edge_ids", ()),
|
||||
"first_level_boundary_edge_count": topology.get("cylindrical_feature_boundary_edge_count", 0),
|
||||
"first_level_boundary_vertex_count": topology.get("cylindrical_feature_boundary_vertex_count", 0),
|
||||
"first_level_adjacent_face_ids": topology.get("cylindrical_feature_adjacent_face_ids", ()),
|
||||
"first_level_adjacent_face_count": topology.get("cylindrical_feature_adjacent_face_count", 0),
|
||||
"first_level_face_ids": topology.get("cylindrical_feature_first_level_face_ids", (face_id,)),
|
||||
"first_level_face_count": topology.get("cylindrical_feature_first_level_face_count", 1),
|
||||
"first_level_topology_note": topology.get("first_level_topology_note", ""),
|
||||
"cylindrical_feature_side_face_ids": topology.get("cylindrical_feature_side_face_ids", (face_id,)),
|
||||
"cylindrical_feature_side_face_count": topology.get("cylindrical_feature_side_face_count", 1),
|
||||
"cylindrical_feature_boundary_edge_ids": topology.get("cylindrical_feature_boundary_edge_ids", ()),
|
||||
"cylindrical_feature_boundary_edge_count": topology.get("cylindrical_feature_boundary_edge_count", 0),
|
||||
"cylindrical_feature_boundary_vertex_count": topology.get("cylindrical_feature_boundary_vertex_count", 0),
|
||||
"cylindrical_feature_adjacent_face_ids": topology.get("cylindrical_feature_adjacent_face_ids", ()),
|
||||
"cylindrical_feature_adjacent_face_count": topology.get("cylindrical_feature_adjacent_face_count", 0),
|
||||
"cylindrical_feature_end_face_ids": topology.get("cylindrical_feature_end_face_ids", ()),
|
||||
"cylindrical_feature_end_face_count": topology.get("cylindrical_feature_end_face_count", 0),
|
||||
"cylindrical_feature_bottom_face_ids": topology.get("cylindrical_feature_bottom_face_ids", ()),
|
||||
"cylindrical_feature_bottom_face_count": topology.get("cylindrical_feature_bottom_face_count", 0),
|
||||
"cylindrical_feature_opening_face_ids": topology.get("cylindrical_feature_opening_face_ids", ()),
|
||||
"cylindrical_feature_opening_face_count": topology.get("cylindrical_feature_opening_face_count", 0),
|
||||
"cylindrical_feature_slot_boundary_face_ids": topology.get(
|
||||
"cylindrical_feature_slot_boundary_face_ids",
|
||||
(),
|
||||
),
|
||||
"cylindrical_feature_slot_boundary_face_count": topology.get(
|
||||
"cylindrical_feature_slot_boundary_face_count",
|
||||
0,
|
||||
),
|
||||
**self._first_level_fact_selection_fields(face_id, scope="cylindrical-feature"),
|
||||
}
|
||||
|
||||
def _feature_info_for_selected_face(self, face_id: int, fallback_info: dict[str, object]) -> dict[str, object]:
|
||||
@@ -275,6 +360,8 @@ class WindowStateMixin:
|
||||
root_info = self.model.feature_info(face_id)
|
||||
if str(root_info.get("surface", "") or "") == "plane":
|
||||
root_info.update(self._face_first_level_selection_fields(face_id))
|
||||
elif str(root_info.get("surface", "") or "") == "cylinder" and detection_level != "current-only":
|
||||
root_info.update(self._cylindrical_first_level_selection_fields(face_id))
|
||||
associated: list[dict[str, object]] = []
|
||||
if detection_level in {"associated-only", "secondary"}:
|
||||
try:
|
||||
@@ -323,6 +410,10 @@ class WindowStateMixin:
|
||||
),
|
||||
}
|
||||
)
|
||||
try:
|
||||
info.update(self.model._recognition_summary_fields(info))
|
||||
except Exception:
|
||||
pass
|
||||
return info
|
||||
|
||||
def _current_feature_detection_level(self) -> str:
|
||||
@@ -1615,6 +1706,38 @@ class WindowStateMixin:
|
||||
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"
|
||||
recognition_fields_present = any(
|
||||
key in action_info
|
||||
for key in (
|
||||
"recognition_score",
|
||||
"recognition_confidence",
|
||||
"recognition_risk",
|
||||
"recognition_blockers",
|
||||
)
|
||||
)
|
||||
recognition_score = _int_or_none(action_info.get("recognition_score"))
|
||||
recognition_confidence = str(action_info.get("recognition_confidence") or "").strip()
|
||||
recognition_risk = str(action_info.get("recognition_risk") or "").strip()
|
||||
recognition_blockers = str(action_info.get("recognition_blockers") or "").strip()
|
||||
analytic_surface_recognition_ready = (
|
||||
not recognition_fields_present
|
||||
or (
|
||||
recognition_risk != "blocked"
|
||||
and not recognition_blockers
|
||||
and recognition_confidence not in {"low", "none"}
|
||||
and (recognition_score is None or recognition_score >= 56)
|
||||
)
|
||||
)
|
||||
analytic_surface_recognition_reason = ""
|
||||
if not analytic_surface_recognition_ready:
|
||||
score_text = _format_float(float(recognition_score)) if recognition_score is not None else "未知"
|
||||
confidence_text = recognition_confidence or "未知"
|
||||
risk_text = recognition_risk or "未知"
|
||||
analytic_surface_recognition_reason = (
|
||||
f"当前解析曲面识别还不够稳定:评分 {score_text},置信度 {confidence_text},风险 {risk_text}。"
|
||||
)
|
||||
if recognition_blockers:
|
||||
analytic_surface_recognition_reason = f"{analytic_surface_recognition_reason} 限制:{recognition_blockers}"
|
||||
is_full_cylinder = angular_span is not None and angular_span >= math.tau * 0.92
|
||||
is_slot_or_half_hole = (
|
||||
is_hole_or_groove
|
||||
@@ -1970,6 +2093,62 @@ class WindowStateMixin:
|
||||
)
|
||||
|
||||
if has_face:
|
||||
topology_depth = _int_or_none(action_info.get("topology_relation_depth"))
|
||||
is_cylindrical_topology = bool(
|
||||
is_cylinder
|
||||
and topology_depth == 1
|
||||
and (
|
||||
"cylindrical_feature_side_face_count" in action_info
|
||||
or "cylindrical-feature" in str(action_info.get("topology_relation_model") or "")
|
||||
)
|
||||
)
|
||||
if is_cylindrical_topology:
|
||||
side_count = _int_or_none(action_info.get("cylindrical_feature_side_face_count")) or 0
|
||||
boundary_edge_count = _int_or_none(action_info.get("cylindrical_feature_boundary_edge_count")) or 0
|
||||
boundary_vertex_count = _int_or_none(action_info.get("cylindrical_feature_boundary_vertex_count")) or (
|
||||
_int_or_none(action_info.get("first_level_boundary_vertex_count")) or 0
|
||||
)
|
||||
adjacent_face_count = _int_or_none(action_info.get("cylindrical_feature_adjacent_face_count")) or 0
|
||||
end_face_count = _int_or_none(action_info.get("cylindrical_feature_end_face_count")) or 0
|
||||
bottom_face_count = _int_or_none(action_info.get("cylindrical_feature_bottom_face_count")) or 0
|
||||
opening_face_count = _int_or_none(action_info.get("cylindrical_feature_opening_face_count")) or 0
|
||||
slot_boundary_face_count = (
|
||||
_int_or_none(action_info.get("cylindrical_feature_slot_boundary_face_count")) or 0
|
||||
)
|
||||
relation_parts = [
|
||||
f"侧壁 Face {side_count} 个",
|
||||
f"边界 Edge {boundary_edge_count} 条",
|
||||
f"边界 Vertex {boundary_vertex_count} 个",
|
||||
f"共享边相邻 Face {adjacent_face_count} 个",
|
||||
]
|
||||
detail_parts = []
|
||||
if end_face_count:
|
||||
detail_parts.append(f"端面/开口 Face {end_face_count} 个")
|
||||
if bottom_face_count:
|
||||
detail_parts.append(f"底面 Face {bottom_face_count} 个")
|
||||
if opening_face_count:
|
||||
detail_parts.append(f"开口 Face {opening_face_count} 个")
|
||||
if slot_boundary_face_count:
|
||||
detail_parts.append(f"槽边界 Face {slot_boundary_face_count} 个")
|
||||
topology_note = str(action_info.get("first_level_topology_note") or "").strip()
|
||||
ignored_note = str(action_info.get("topology_ignored_relation_note") or "").strip()
|
||||
status_message = str(action_info.get("topology_relation_message") or "").strip()
|
||||
topology_tip = "\n".join(
|
||||
item
|
||||
for item in (
|
||||
topology_note,
|
||||
ignored_note,
|
||||
status_message,
|
||||
"当前阶段只把圆柱侧壁及其共享边直接相邻 Face 作为一级关系,不会自动沿相邻面继续传播到二级、三级关系。",
|
||||
)
|
||||
if item
|
||||
)
|
||||
add_readonly_spec(
|
||||
key="cylindrical_feature_first_level_topology",
|
||||
label="一级关系",
|
||||
text=";".join(relation_parts + detail_parts) + "。",
|
||||
tip=topology_tip,
|
||||
)
|
||||
if is_hole_or_groove:
|
||||
if is_slot_or_half_hole:
|
||||
add_readonly_spec(
|
||||
@@ -2025,7 +2204,6 @@ class WindowStateMixin:
|
||||
),
|
||||
)
|
||||
elif is_plane or is_shell_candidate:
|
||||
topology_depth = _int_or_none(action_info.get("topology_relation_depth"))
|
||||
if topology_depth == 1:
|
||||
same_domain_count = _int_or_none(action_info.get("same_domain_face_count")) or 0
|
||||
boundary_edge_count = _int_or_none(action_info.get("first_level_boundary_edge_count")) or 0
|
||||
@@ -3037,18 +3215,32 @@ class WindowStateMixin:
|
||||
current_boss_height = _float_or_none(action_info.get("same_domain_height_estimate"))
|
||||
if current_boss_height is None:
|
||||
current_boss_height = _float_or_none(action_info.get("height_estimate"))
|
||||
boss_height_can_local = bool(
|
||||
is_full_cylinder
|
||||
and current_boss_height is not None
|
||||
and (
|
||||
_int_values(action_info.get("feature_start_end_face_ids"))
|
||||
or _int_values(action_info.get("feature_end_end_face_ids"))
|
||||
)
|
||||
)
|
||||
boss_height_can_scale = bool(
|
||||
current_boss_height is not None
|
||||
and current_boss_height > 0
|
||||
and _triple_or_none(action_info.get("axis_point")) is not None
|
||||
and _triple_or_none(action_info.get("axis")) is not None
|
||||
)
|
||||
add_scoped_spec(
|
||||
key="boss_height",
|
||||
label="高度",
|
||||
current_raw=current_boss_height if current_boss_height is not None else "",
|
||||
target_text=numeric_text(current_boss_height),
|
||||
scope_default="local",
|
||||
scope_default="owning" if boss_height_can_scale else "local",
|
||||
scope_modes={
|
||||
"local": {
|
||||
"label": "推拉端盖",
|
||||
"action": "resize_boss_height",
|
||||
"target_attr": "boss_height_input",
|
||||
"enabled": bool(is_full_cylinder and current_boss_height is not None),
|
||||
"enabled": boss_height_can_local,
|
||||
"enabled_tip": "输入完整圆柱凸台的目标高度;点击修改时程序会再计算并确认可推拉的凸台端盖 Face。",
|
||||
"disabled_tip": "当前凸台候选缺少稳定高度或端盖信息,暂不放行高度修改。",
|
||||
"range_hint": relative_range_hint(current_boss_height, 0.3, 0.8),
|
||||
@@ -3057,12 +3249,7 @@ class WindowStateMixin:
|
||||
"label": "调整整个特征",
|
||||
"action": "resize_cylindrical_height_owning_scale",
|
||||
"target_attr": "boss_height_input",
|
||||
"enabled": bool(
|
||||
current_boss_height is not None
|
||||
and current_boss_height > 0
|
||||
and _triple_or_none(action_info.get("axis_point")) is not None
|
||||
and _triple_or_none(action_info.get("axis")) is not None
|
||||
),
|
||||
"enabled": boss_height_can_scale,
|
||||
"enabled_tip": "输入目标高度;程序会沿圆柱轴向整体缩放所属特征或 Solid,不是推拉凸台端盖。",
|
||||
"disabled_tip": "当前凸台缺少稳定高度、轴线或缩放中心,不能按高度整体缩放所属对象。",
|
||||
"range_hint": (
|
||||
@@ -3153,12 +3340,18 @@ class WindowStateMixin:
|
||||
current_cylinder_height = _float_or_none(action_info.get("same_domain_height_estimate"))
|
||||
if current_cylinder_height is None:
|
||||
current_cylinder_height = _float_or_none(action_info.get("height_estimate"))
|
||||
cylinder_height_can_scale = bool(
|
||||
current_cylinder_height is not None
|
||||
and current_cylinder_height > 0
|
||||
and generic_scale_axis_point is not None
|
||||
and generic_scale_axis_direction is not None
|
||||
)
|
||||
add_scoped_spec(
|
||||
key="cylinder_height",
|
||||
label="高度",
|
||||
current_raw=current_cylinder_height if current_cylinder_height is not None else "",
|
||||
target_text=numeric_text(current_cylinder_height),
|
||||
scope_default="local",
|
||||
scope_default="owning" if cylinder_height_can_scale else "local",
|
||||
scope_modes={
|
||||
"local": {
|
||||
"label": "推拉端盖",
|
||||
@@ -3173,12 +3366,7 @@ class WindowStateMixin:
|
||||
"label": "调整整个特征",
|
||||
"action": "resize_cylindrical_height_owning_scale",
|
||||
"target_attr": "boss_height_input",
|
||||
"enabled": bool(
|
||||
current_cylinder_height is not None
|
||||
and current_cylinder_height > 0
|
||||
and generic_scale_axis_point is not None
|
||||
and generic_scale_axis_direction is not None
|
||||
),
|
||||
"enabled": cylinder_height_can_scale,
|
||||
"enabled_tip": "输入目标高度;程序会沿圆柱轴向整体缩放所属特征或 Solid,不是推拉单个端盖。",
|
||||
"disabled_tip": "当前圆柱缺少稳定高度、轴线或缩放中心,不能按高度整体缩放所属对象。",
|
||||
"range_hint": (
|
||||
@@ -3295,6 +3483,14 @@ class WindowStateMixin:
|
||||
used=("existing_fillet_arc_length_estimate", "existing_fillet_angular_span", "angular_span"),
|
||||
**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)
|
||||
|
||||
def analytic_surface_disabled_tip(base: str) -> str:
|
||||
if not analytic_surface_recognition_ready and analytic_surface_recognition_reason:
|
||||
return analytic_surface_recognition_reason
|
||||
return base
|
||||
|
||||
if is_cone:
|
||||
current_reference_radius = _float_or_none(action_info.get("reference_radius"))
|
||||
current_reference_diameter = (
|
||||
@@ -3303,7 +3499,10 @@ class WindowStateMixin:
|
||||
reference_radius_supported, reference_radius_disabled_reason = cone_reference_radius_capability(
|
||||
current_reference_radius
|
||||
)
|
||||
reference_radius_enabled = current_reference_radius is not None and reference_radius_supported
|
||||
reference_radius_enabled = analytic_surface_enabled(
|
||||
current_reference_radius is not None,
|
||||
reference_radius_supported,
|
||||
)
|
||||
add_spec(
|
||||
key="cone_reference_radius",
|
||||
label="参考半径",
|
||||
@@ -3313,7 +3512,7 @@ class WindowStateMixin:
|
||||
target_attr="cone_reference_radius_input",
|
||||
enabled=reference_radius_enabled,
|
||||
enabled_tip="输入圆锥面的目标参考半径;简单圆锥会解析重建,嵌入式锥孔会优先局部重切。",
|
||||
disabled_tip=(
|
||||
disabled_tip=analytic_surface_disabled_tip(
|
||||
reference_radius_disabled_reason
|
||||
or "当前圆锥面缺少稳定参考半径,不能直接修改。"
|
||||
),
|
||||
@@ -3332,9 +3531,12 @@ class WindowStateMixin:
|
||||
target_text=numeric_text(current_reference_diameter),
|
||||
action="resize_cone_reference_radius",
|
||||
target_attr="cone_reference_radius_input",
|
||||
enabled=current_reference_diameter is not None and reference_radius_supported,
|
||||
enabled=analytic_surface_enabled(
|
||||
current_reference_diameter is not None,
|
||||
reference_radius_supported,
|
||||
),
|
||||
enabled_tip="输入圆锥面的目标参考直径;程序会换算为参考半径后选择解析重建或锥孔局部重切。",
|
||||
disabled_tip=(
|
||||
disabled_tip=analytic_surface_disabled_tip(
|
||||
reference_radius_disabled_reason
|
||||
or "当前圆锥面缺少稳定参考直径,不能直接修改。"
|
||||
),
|
||||
@@ -3356,6 +3558,7 @@ class WindowStateMixin:
|
||||
current_reference_radius is not None
|
||||
and current_semi_angle is not None
|
||||
and semi_angle_supported
|
||||
and analytic_surface_recognition_ready
|
||||
)
|
||||
add_spec(
|
||||
key="cone_semi_angle_degrees",
|
||||
@@ -3366,7 +3569,7 @@ class WindowStateMixin:
|
||||
target_attr="cone_reference_radius_input",
|
||||
enabled=semi_angle_enabled,
|
||||
enabled_tip="输入圆锥面的目标半角,单位是度;简单圆锥会解析重建,嵌入式锥孔会优先局部重切。",
|
||||
disabled_tip=(
|
||||
disabled_tip=analytic_surface_disabled_tip(
|
||||
semi_angle_disabled_reason
|
||||
or "当前圆锥面缺少稳定参考半径或半角,不能直接修改半角。"
|
||||
),
|
||||
@@ -3390,9 +3593,9 @@ class WindowStateMixin:
|
||||
target_text=numeric_text(current_sphere_radius),
|
||||
action="resize_sphere_radius",
|
||||
target_attr="sphere_radius_input",
|
||||
enabled=current_sphere_radius is not None,
|
||||
enabled=analytic_surface_enabled(current_sphere_radius is not None),
|
||||
enabled_tip="输入球面的目标半径;程序会围绕球心均匀缩放所属对象。",
|
||||
disabled_tip="当前球面缺少稳定半径,不能直接修改。",
|
||||
disabled_tip=analytic_surface_disabled_tip("当前球面缺少稳定半径,不能直接修改。"),
|
||||
value_type="positive",
|
||||
range_hint=f"这是整体缩放所属对象,不是只替换单个球面的历史半径参数。 {relative_range_hint(current_sphere_radius, 0.25, 0.6)}",
|
||||
used=("radius",),
|
||||
@@ -3405,9 +3608,9 @@ class WindowStateMixin:
|
||||
target_text=numeric_text(current_sphere_diameter),
|
||||
action="resize_sphere_radius",
|
||||
target_attr="sphere_radius_input",
|
||||
enabled=current_sphere_diameter is not None,
|
||||
enabled=analytic_surface_enabled(current_sphere_diameter is not None),
|
||||
enabled_tip="输入球面的目标直径;程序会换算为半径后围绕球心均匀缩放所属对象。",
|
||||
disabled_tip="当前球面缺少稳定直径,不能直接修改。",
|
||||
disabled_tip=analytic_surface_disabled_tip("当前球面缺少稳定直径,不能直接修改。"),
|
||||
value_type="positive",
|
||||
range_hint=f"这是整体缩放所属对象,不是只替换单个球面的历史直径参数。 {relative_range_hint(current_sphere_diameter, 0.25, 0.6)}",
|
||||
target_transform="diameter_to_radius",
|
||||
@@ -3430,9 +3633,9 @@ class WindowStateMixin:
|
||||
target_text=numeric_text(current_major_radius),
|
||||
action="resize_torus_major_radius",
|
||||
target_attr="torus_radius_input",
|
||||
enabled=current_major_radius is not None,
|
||||
enabled=analytic_surface_enabled(current_major_radius is not None),
|
||||
enabled_tip="输入环面的目标主半径;当前版本会围绕环面中心均匀缩放所属对象,主半径和小半径会等比例变化。",
|
||||
disabled_tip="当前环面缺少稳定主半径,不能直接修改。",
|
||||
disabled_tip=analytic_surface_disabled_tip("当前环面缺少稳定主半径,不能直接修改。"),
|
||||
value_type="positive",
|
||||
range_hint=f"这是整体缩放所属对象,不是只改环面主半径;小半径和其它尺寸也会跟随变化。 {relative_range_hint(current_major_radius, 0.25, 0.6)}",
|
||||
used=("major_radius", "feature_torus_major_radius"),
|
||||
@@ -3445,9 +3648,9 @@ class WindowStateMixin:
|
||||
target_text=numeric_text(current_major_diameter),
|
||||
action="resize_torus_major_radius",
|
||||
target_attr="torus_radius_input",
|
||||
enabled=current_major_diameter is not None,
|
||||
enabled=analytic_surface_enabled(current_major_diameter is not None),
|
||||
enabled_tip="输入环面的目标主直径;程序会换算为主半径后整体缩放所属对象。",
|
||||
disabled_tip="当前环面缺少稳定主直径,不能直接修改。",
|
||||
disabled_tip=analytic_surface_disabled_tip("当前环面缺少稳定主直径,不能直接修改。"),
|
||||
value_type="positive",
|
||||
range_hint=f"这是整体缩放所属对象,不是只改环面主直径;小半径和其它尺寸也会跟随变化。 {relative_range_hint(current_major_diameter, 0.25, 0.6)}",
|
||||
target_transform="diameter_to_radius",
|
||||
@@ -3461,9 +3664,9 @@ class WindowStateMixin:
|
||||
target_text=numeric_text(current_minor_radius),
|
||||
action="resize_torus_minor_radius",
|
||||
target_attr="torus_radius_input",
|
||||
enabled=current_minor_radius is not None,
|
||||
enabled=analytic_surface_enabled(current_minor_radius is not None),
|
||||
enabled_tip="输入环面的目标小半径;当前版本会围绕环面中心均匀缩放所属对象,主半径和小半径会等比例变化。",
|
||||
disabled_tip="当前环面缺少稳定小半径,不能直接修改。",
|
||||
disabled_tip=analytic_surface_disabled_tip("当前环面缺少稳定小半径,不能直接修改。"),
|
||||
value_type="positive",
|
||||
range_hint=f"这是整体缩放所属对象,不是只改环面小半径;主半径和其它尺寸也会跟随变化。 {relative_range_hint(current_minor_radius, 0.25, 0.6)}",
|
||||
used=("minor_radius", "feature_torus_minor_radius"),
|
||||
@@ -3476,9 +3679,9 @@ class WindowStateMixin:
|
||||
target_text=numeric_text(current_minor_diameter),
|
||||
action="resize_torus_minor_radius",
|
||||
target_attr="torus_radius_input",
|
||||
enabled=current_minor_diameter is not None,
|
||||
enabled=analytic_surface_enabled(current_minor_diameter is not None),
|
||||
enabled_tip="输入环面的目标小直径;程序会换算为小半径后整体缩放所属对象。",
|
||||
disabled_tip="当前环面缺少稳定小直径,不能直接修改。",
|
||||
disabled_tip=analytic_surface_disabled_tip("当前环面缺少稳定小直径,不能直接修改。"),
|
||||
value_type="positive",
|
||||
range_hint=f"这是整体缩放所属对象,不是只改环面小直径;主半径和其它尺寸也会跟随变化。 {relative_range_hint(current_minor_diameter, 0.25, 0.6)}",
|
||||
target_transform="diameter_to_radius",
|
||||
|
||||
Reference in New Issue
Block a user