feat: 完善 Face 一级关系编辑和稳定性
This commit is contained in:
+481
-28
@@ -28,6 +28,57 @@ PROPERTY_SCOPE_COLUMN = 2
|
||||
PROPERTY_TARGET_COLUMN = 3
|
||||
PROPERTY_ACTION_COLUMN = 4
|
||||
|
||||
FEATURE_EDIT_SEMANTICS_KEYS = {
|
||||
"face_first_level_topology",
|
||||
"slot_edit_semantics",
|
||||
"hole_edit_semantics",
|
||||
"boss_edit_semantics",
|
||||
"existing_fillet_edit_semantics",
|
||||
"analytic_surface_edit_semantics",
|
||||
"face_edit_semantics",
|
||||
}
|
||||
|
||||
|
||||
def _feature_dimension_keys(action_info: dict[str, object]) -> tuple[str, ...]:
|
||||
"""Return the independent, user-facing dimensions for a feature candidate."""
|
||||
surface = str(action_info.get("surface", "") or "")
|
||||
feature_guess = str(action_info.get("feature_guess", "") or "")
|
||||
angular_span = _float_or_none(action_info.get("angular_span"))
|
||||
|
||||
if surface == "plane":
|
||||
if action_info.get("prismatic_profile_status") == "candidate":
|
||||
keys = ["local_face_width", "local_face_height"]
|
||||
if action_info.get("prismatic_extrusion_status") == "candidate":
|
||||
keys.append("shell_thickness_estimate")
|
||||
return tuple(keys)
|
||||
if action_info.get("shell_region_status") == "candidate":
|
||||
return ("shell_thickness_estimate",)
|
||||
return ("face_target_normal_position",)
|
||||
|
||||
if surface == "cylinder":
|
||||
is_partial = angular_span is not None and angular_span < math.tau * 0.92
|
||||
if feature_guess == "hole/groove candidate":
|
||||
if is_partial:
|
||||
return (
|
||||
"slot_chord_width_estimate",
|
||||
"slot_sagitta_depth_estimate",
|
||||
"slot_total_length_estimate",
|
||||
)
|
||||
return ("diameter", "hole_depth_estimate")
|
||||
if feature_guess == "boss/outer-round candidate":
|
||||
return ("boss_diameter", "boss_height")
|
||||
if feature_guess == "round/fillet candidate":
|
||||
return ("existing_fillet_radius_estimate",)
|
||||
return ("generic_cylinder_diameter", "cylinder_height")
|
||||
|
||||
if surface == "cone":
|
||||
return ("cone_reference_radius", "cone_semi_angle_degrees")
|
||||
if surface == "sphere":
|
||||
return ("sphere_radius",)
|
||||
if surface == "torus":
|
||||
return ("torus_major_radius", "torus_minor_radius")
|
||||
return ()
|
||||
|
||||
|
||||
def _record_message_field(message: str | None, key: str) -> str | None:
|
||||
if not message:
|
||||
@@ -109,9 +160,51 @@ class WindowStateMixin:
|
||||
enriched["pick_position"] = pick_position
|
||||
return enriched
|
||||
|
||||
def _face_first_level_selection_fields(self, face_id: int) -> dict[str, object]:
|
||||
if self.model is None:
|
||||
return {}
|
||||
try:
|
||||
topology = self.model.face_first_level_topology(face_id)
|
||||
except Exception as exc:
|
||||
return {
|
||||
"topology_relation_depth": 1,
|
||||
"topology_relation_model": "STEP/B-Rep shared-edge first-level",
|
||||
"topology_relation_status": "unavailable",
|
||||
"topology_relation_message": str(exc),
|
||||
"same_domain_face_count": 1,
|
||||
"first_level_boundary_edge_count": 0,
|
||||
"first_level_boundary_vertex_count": 0,
|
||||
"first_level_adjacent_face_count": 0,
|
||||
"first_level_topology_note": (
|
||||
"当前 Face 的一级拓扑关系暂时无法确认;只改当前面的局部重建会在计划阶段再次检查。"
|
||||
),
|
||||
}
|
||||
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", ""),
|
||||
"same_domain_face_ids": topology.get("same_domain_face_ids", (face_id,)),
|
||||
"same_domain_face_count": topology.get("same_domain_face_count", 1),
|
||||
"same_domain_region_kind": topology.get("same_domain_region_kind", "single-face"),
|
||||
"first_level_boundary_edge_ids": topology.get("first_level_boundary_edge_ids", ()),
|
||||
"first_level_boundary_edge_count": topology.get("first_level_boundary_edge_count", 0),
|
||||
"first_level_boundary_vertex_count": topology.get("first_level_boundary_vertex_count", 0),
|
||||
"first_level_adjacent_face_ids": topology.get("first_level_adjacent_face_ids", ()),
|
||||
"first_level_adjacent_face_count": topology.get("first_level_adjacent_face_count", 0),
|
||||
"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", ""),
|
||||
}
|
||||
|
||||
def _feature_info_for_selected_face(self, face_id: int, fallback_info: dict[str, object]) -> dict[str, object]:
|
||||
if self.model is None:
|
||||
return dict(fallback_info)
|
||||
if "associated_feature_infos" in fallback_info:
|
||||
return dict(fallback_info)
|
||||
surface = str(fallback_info.get("surface", "") or "")
|
||||
if surface not in FACE_SELECTION_FEATURE_INFO_SURFACES:
|
||||
return dict(fallback_info)
|
||||
@@ -153,7 +246,7 @@ class WindowStateMixin:
|
||||
info.setdefault("feature_edit_actions", "可查看圆柱直径/半径;复杂语义需要手动扫描或执行计划确认")
|
||||
elif surface == "cone":
|
||||
info.setdefault("feature_type", "圆锥面候选")
|
||||
info.setdefault("feature_edit_actions", "修改圆锥参考半径/直径(整体缩放)")
|
||||
info.setdefault("feature_edit_actions", "修改圆锥参考半径/直径/半角;程序会按几何选择重建或局部重切")
|
||||
elif surface == "sphere":
|
||||
info.setdefault("feature_type", "球面候选")
|
||||
info.setdefault("feature_edit_actions", "修改球面半径/直径(整体缩放)")
|
||||
@@ -168,8 +261,139 @@ class WindowStateMixin:
|
||||
):
|
||||
if key in fallback_info:
|
||||
info.setdefault(key, fallback_info[key])
|
||||
if surface == "plane":
|
||||
info.update(self._face_first_level_selection_fields(face_id))
|
||||
return info
|
||||
|
||||
def _feature_context_info(self, face_id: int) -> dict[str, object]:
|
||||
if self.model is None:
|
||||
return {}
|
||||
detection_level = self._current_feature_detection_level()
|
||||
if detection_level == "current-only":
|
||||
root_info = self._feature_info_for_selected_face(face_id, self.model.quick_face_info(face_id))
|
||||
else:
|
||||
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))
|
||||
associated: list[dict[str, object]] = []
|
||||
if detection_level in {"associated-only", "secondary"}:
|
||||
try:
|
||||
associated = self.model.associated_feature_infos(face_id)
|
||||
except Exception:
|
||||
associated = []
|
||||
if detection_level == "secondary":
|
||||
associated = self._secondary_associated_feature_infos(face_id, associated)
|
||||
|
||||
highlight_ids = set(_int_values(root_info.get("feature_highlight_face_ids")) or [face_id])
|
||||
for item in associated:
|
||||
highlight_ids.update(_int_values(item.get("feature_highlight_face_ids")))
|
||||
source_id = _int_or_none(item.get("association_source_face_id"))
|
||||
if source_id is not None:
|
||||
highlight_ids.add(source_id)
|
||||
|
||||
level_label = (
|
||||
"二级特征"
|
||||
if detection_level == "secondary"
|
||||
else ("相邻特征" if detection_level == "associated-only" else "当前特征")
|
||||
)
|
||||
info = dict(root_info)
|
||||
info.update(
|
||||
{
|
||||
"associated_feature_infos": associated,
|
||||
"associated_feature_count": len(associated),
|
||||
"feature_detection_level": level_label,
|
||||
"associated_feature_face_ids": tuple(
|
||||
sorted(
|
||||
{
|
||||
int(item.get("association_source_face_id", -1))
|
||||
for item in associated
|
||||
if _int_or_none(item.get("association_source_face_id")) is not None
|
||||
}
|
||||
)
|
||||
),
|
||||
"feature_highlight_face_ids": tuple(sorted(highlight_ids)),
|
||||
"feature_context_note": (
|
||||
f"已按“{level_label}”沿共享边拓扑探测当前特征及 {len(associated)} 个局部关联特征;关联尺寸可在同一参数表中直接修改。"
|
||||
if associated
|
||||
else (
|
||||
"当前为轻量识别:只读取被点击对象本身,不自动扫描周边拓扑;需要更多关联时可切换到“探测相邻特征”。"
|
||||
if detection_level == "current-only"
|
||||
else f"已按“{level_label}”沿共享边拓扑探测局部邻域,未发现额外的可参数化关联特征。"
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
return info
|
||||
|
||||
def _current_feature_detection_level(self) -> str:
|
||||
combo = getattr(self, "feature_detection_combo", None)
|
||||
if isinstance(combo, NoWheelComboBox):
|
||||
value = combo.currentData()
|
||||
if isinstance(value, str) and value:
|
||||
return value
|
||||
return str(getattr(self, "feature_detection_level", "current-only") or "current-only")
|
||||
|
||||
def _on_feature_detection_level_changed(self) -> None:
|
||||
self.feature_detection_level = self._current_feature_detection_level()
|
||||
if self.model is None or self.selected_kind != "feature" or self.selected_face_id is None:
|
||||
return
|
||||
try:
|
||||
info = self._feature_context_info(self.selected_face_id)
|
||||
except Exception:
|
||||
return
|
||||
self.current_info_values = dict(info)
|
||||
self.current_info_text = "\n".join(f"{INFO_LABELS.get(key, key)}: {_format_value(value)}" for key, value in info.items())
|
||||
self._refresh_property_editor()
|
||||
if hasattr(self, "_highlight_faces"):
|
||||
self._highlight_faces(_int_values(info.get("feature_highlight_face_ids")) or [self.selected_face_id])
|
||||
self._update_selected_object_title()
|
||||
|
||||
def _secondary_associated_feature_infos(
|
||||
self,
|
||||
source_face_id: int,
|
||||
direct_infos: list[dict[str, object]],
|
||||
) -> list[dict[str, object]]:
|
||||
if self.model is None:
|
||||
return list(direct_infos)
|
||||
results: list[dict[str, object]] = []
|
||||
seen: set[tuple[str, tuple[int, ...]]] = set()
|
||||
|
||||
def add(info: dict[str, object]) -> None:
|
||||
source_id = _int_or_none(info.get("association_source_face_id"))
|
||||
if source_id is None or source_id == source_face_id:
|
||||
return
|
||||
face_ids = tuple(sorted(_int_values(info.get("feature_highlight_face_ids")) or [source_id]))
|
||||
identity = (str(info.get("feature_type", "") or info.get("feature_guess", "")), face_ids)
|
||||
if identity in seen:
|
||||
return
|
||||
seen.add(identity)
|
||||
results.append(dict(info))
|
||||
|
||||
for info in direct_infos:
|
||||
add(info)
|
||||
for info in list(results):
|
||||
parent_id = _int_or_none(info.get("association_source_face_id"))
|
||||
if parent_id is None:
|
||||
continue
|
||||
try:
|
||||
for nested in self.model.associated_feature_infos(
|
||||
parent_id,
|
||||
max_depth=2,
|
||||
max_scan_faces=48,
|
||||
max_features=6,
|
||||
):
|
||||
add(nested)
|
||||
except Exception:
|
||||
continue
|
||||
results.sort(
|
||||
key=lambda item: (
|
||||
int(item.get("association_priority", 9)),
|
||||
int(item.get("association_hop_count", 99)),
|
||||
int(item.get("association_source_face_id", 0)),
|
||||
)
|
||||
)
|
||||
return results[:14]
|
||||
|
||||
def _selection_status(self, message: str, pick_position: tuple[float, float, float] | None) -> str:
|
||||
if pick_position is None:
|
||||
return message
|
||||
@@ -238,7 +462,7 @@ class WindowStateMixin:
|
||||
)
|
||||
self._set_control_state(
|
||||
self.reload_button,
|
||||
has_loaded_model and not busy,
|
||||
not busy and bool(getattr(self, "step_path", None)),
|
||||
"按当前显示路径读取 STEP 模型。",
|
||||
wait_or_load_tip,
|
||||
)
|
||||
@@ -788,13 +1012,22 @@ class WindowStateMixin:
|
||||
def _update_selected_object_title(self) -> None:
|
||||
if not hasattr(self, "object_edit_box"):
|
||||
return
|
||||
self.object_edit_box.setTitle(f"当前选中对象:{self._selected_object_title_suffix()}")
|
||||
section_label = "几何对象(高级)" if self.selected_kind in {"face", "edge", "solid", "part"} else "特征参数"
|
||||
self.object_edit_box.setTitle(f"{section_label}:{self._selected_object_title_suffix()}")
|
||||
|
||||
def _selected_object_title_suffix(self) -> str:
|
||||
if self.selected_kind == "part" and self.selected_part_id is not None:
|
||||
return f"零件 {self.selected_part_id}"
|
||||
if self.selected_kind == "solid" and self.selected_solid_id is not None:
|
||||
return f"Solid {self.selected_solid_id}"
|
||||
if self.selected_kind == "feature" and self.selected_face_id is not None:
|
||||
feature_label = self._selected_feature_label() or "特征"
|
||||
confidence = str(self.current_info_values.get("confidence", "") or "")
|
||||
confidence_label = {"high": "高", "medium": "中", "low": "低"}.get(confidence, "")
|
||||
confidence_suffix = f"(置信度:{confidence_label})" if confidence_label else ""
|
||||
related_count = int(self.current_info_values.get("associated_feature_count", 0) or 0)
|
||||
related_suffix = f" · 关联 {related_count} 项" if related_count else ""
|
||||
return f"{feature_label}{confidence_suffix} · 来源 Face {self.selected_face_id}{related_suffix}"
|
||||
if self.selected_kind == "feature" and self.selected_face_id is not None:
|
||||
feature_label = self._selected_feature_label()
|
||||
if feature_label:
|
||||
@@ -1147,10 +1380,10 @@ class WindowStateMixin:
|
||||
self.property_expand_button.setVisible(has_hidden_rows)
|
||||
if expanded:
|
||||
self.property_expand_button.setText(f"收起到前 {collapsed_rows} 项")
|
||||
self.property_expand_button.setToolTip("收起当前选中对象属性表,让面板只保留最常用的前几行。")
|
||||
self.property_expand_button.setToolTip("收起参数列表,只保留最常用的前几项。")
|
||||
else:
|
||||
self.property_expand_button.setText(f"展开全部属性 ({row_count} 项)")
|
||||
self.property_expand_button.setToolTip("展开当前选中对象的完整属性表;参数化建模按钮会继续留在下方。")
|
||||
self.property_expand_button.setText(f"展开全部参数 ({row_count} 项)")
|
||||
self.property_expand_button.setToolTip("展开完整参数列表;参数化建模按钮会继续留在下方。")
|
||||
|
||||
def toggle_property_table_expanded(self) -> None:
|
||||
if not hasattr(self, "property_table"):
|
||||
@@ -1172,6 +1405,8 @@ class WindowStateMixin:
|
||||
action_info: dict[str, object],
|
||||
) -> list[dict[str, object]]:
|
||||
editable_specs, used_keys = self._editable_property_specs(action_info)
|
||||
if self.selected_kind == "feature":
|
||||
return self._feature_context_property_specs(editable_specs, action_info)
|
||||
specs = list(editable_specs)
|
||||
for key, value in self._ordered_property_info_items(info):
|
||||
if key in used_keys:
|
||||
@@ -1180,7 +1415,7 @@ class WindowStateMixin:
|
||||
{
|
||||
"key": key,
|
||||
"label": INFO_LABELS.get(key, key),
|
||||
"current_text": _format_value(value),
|
||||
"current_text": _format_info_value(key, value),
|
||||
"current_raw": value,
|
||||
"target_text": "",
|
||||
"editable": False,
|
||||
@@ -1204,6 +1439,86 @@ class WindowStateMixin:
|
||||
)
|
||||
return specs
|
||||
|
||||
def _feature_property_specs(
|
||||
self,
|
||||
specs: list[dict[str, object]],
|
||||
action_info: dict[str, object],
|
||||
) -> list[dict[str, object]]:
|
||||
allowed_keys = _feature_dimension_keys(action_info)
|
||||
spec_by_key = {str(spec.get("key", "")): spec for spec in specs}
|
||||
dimensions: list[dict[str, object]] = []
|
||||
for key in allowed_keys:
|
||||
spec = spec_by_key.get(key)
|
||||
if spec is None or not bool(spec.get("editable")) or not bool(spec.get("enabled")):
|
||||
continue
|
||||
dimension = dict(spec)
|
||||
dimension["parameter_role"] = "dimension"
|
||||
if action_info.get("prismatic_profile_status") == "candidate":
|
||||
label_overrides = {
|
||||
"local_face_width": "长度",
|
||||
"local_face_height": "宽度",
|
||||
"shell_thickness_estimate": "高度/深度",
|
||||
}
|
||||
if key in label_overrides:
|
||||
dimension["label"] = label_overrides[key]
|
||||
dimensions.append(dimension)
|
||||
|
||||
explanations = [
|
||||
dict(spec)
|
||||
for spec in specs
|
||||
if str(spec.get("key", "")) in FEATURE_EDIT_SEMANTICS_KEYS
|
||||
]
|
||||
if not dimensions:
|
||||
dimensions.append(
|
||||
{
|
||||
"key": "no_editable_feature_dimensions",
|
||||
"label": "可变尺寸",
|
||||
"current_text": "未识别到可靠的独立尺寸",
|
||||
"current_raw": "",
|
||||
"target_text": "",
|
||||
"editable": False,
|
||||
"enabled": False,
|
||||
"status_text": "说明",
|
||||
"disabled_tip": (
|
||||
"当前几何仍可在诊断信息中查看,但不会把面积、中心、包围盒或底层曲面参数"
|
||||
"伪装成特征设计尺寸。"
|
||||
),
|
||||
}
|
||||
)
|
||||
return dimensions + explanations
|
||||
|
||||
def _feature_context_property_specs(
|
||||
self,
|
||||
root_specs: list[dict[str, object]],
|
||||
action_info: dict[str, object],
|
||||
) -> list[dict[str, object]]:
|
||||
root_rows = self._feature_property_specs(root_specs, action_info)
|
||||
associated = action_info.get("associated_feature_infos")
|
||||
if not isinstance(associated, (list, tuple)) or not associated:
|
||||
return root_rows
|
||||
|
||||
root_dimensions = [dict(spec) for spec in root_rows if spec.get("parameter_role") == "dimension"]
|
||||
root_explanations = [dict(spec) for spec in root_rows if spec.get("parameter_role") != "dimension"]
|
||||
related_rows: list[dict[str, object]] = []
|
||||
for index, related_info in enumerate(associated, start=1):
|
||||
if not isinstance(related_info, dict):
|
||||
continue
|
||||
related_specs, _used = self._editable_property_specs(related_info)
|
||||
feature_label = str(related_info.get("feature_type") or related_info.get("feature_guess") or "关联特征")
|
||||
source_face_id = _int_or_none(related_info.get("association_source_face_id"))
|
||||
for spec in self._feature_property_specs(related_specs, related_info):
|
||||
if spec.get("parameter_role") != "dimension":
|
||||
continue
|
||||
related = dict(spec)
|
||||
related["label"] = f"{feature_label} · {spec.get('label', '')}"
|
||||
related["scope_text"] = f"关联 Face {source_face_id}" if source_face_id is not None else f"关联特征 {index}"
|
||||
related["source_face_id"] = source_face_id
|
||||
related["source_feature_info"] = dict(related_info)
|
||||
related["association_index"] = index
|
||||
related_rows.append(related)
|
||||
|
||||
return root_dimensions + related_rows + root_explanations
|
||||
|
||||
def _ordered_property_info_items(self, info: dict[str, object]) -> list[tuple[str, object]]:
|
||||
items = self._ordered_info_items(info)
|
||||
if not self._is_feature_like_info(info):
|
||||
@@ -1456,6 +1771,75 @@ class WindowStateMixin:
|
||||
return base
|
||||
return f"{base} 当前不能只改当前面的原因:{local_face_deform_blocker}"
|
||||
|
||||
def cone_semi_angle_capability(current_angle_degrees: float | None) -> tuple[bool, str]:
|
||||
if current_angle_degrees is None:
|
||||
return True, ""
|
||||
if not (
|
||||
has_model
|
||||
and is_cone
|
||||
and self.selected_face_id is not None
|
||||
and hasattr(self.model, "conical_semi_angle_plan")
|
||||
):
|
||||
return True, ""
|
||||
delta = max(1.0, min(5.0, abs(current_angle_degrees) * 0.25))
|
||||
probe_target = current_angle_degrees + delta
|
||||
if probe_target >= 89.0:
|
||||
probe_target = max(0.1, current_angle_degrees - delta)
|
||||
if abs(probe_target - current_angle_degrees) <= 1e-7:
|
||||
return False, "当前圆锥半角太接近允许范围边界,不能稳定探测可编辑性。"
|
||||
try:
|
||||
plan = self.model.conical_semi_angle_plan(int(self.selected_face_id), probe_target)
|
||||
except Exception as exc:
|
||||
return False, f"无法确认当前圆锥半角是否可稳定修改:{exc}"
|
||||
strategy = str(plan.get("resize_strategy") or "")
|
||||
if strategy.startswith("analytic-cone-rebuild") or strategy.startswith("bounded-cone-recut"):
|
||||
return True, ""
|
||||
if strategy.startswith("blocked-complex-cone-semi-angle") or strategy == "radial-affine-scale-cone-semi-angle":
|
||||
message = str(plan.get("message") or "").strip()
|
||||
if message:
|
||||
return False, message
|
||||
return (
|
||||
False,
|
||||
"当前 Face 是圆锥面/拔模面,但不是简单圆锥,也不是可识别的锥孔/沉孔;"
|
||||
"当前版本不把它作为稳定的圆锥半角参数开放。",
|
||||
)
|
||||
if str(plan.get("status") or "") == "blocked":
|
||||
return False, str(plan.get("message") or "当前圆锥半角不能稳定修改。")
|
||||
return True, ""
|
||||
|
||||
def cone_reference_radius_capability(current_radius: float | None) -> tuple[bool, str]:
|
||||
if current_radius is None or current_radius <= 0:
|
||||
return True, ""
|
||||
if not (
|
||||
has_model
|
||||
and is_cone
|
||||
and self.selected_face_id is not None
|
||||
and hasattr(self.model, "conical_reference_radius_plan")
|
||||
):
|
||||
return True, ""
|
||||
probe_target = current_radius * 1.05
|
||||
try:
|
||||
plan = self.model.conical_reference_radius_plan(int(self.selected_face_id), probe_target)
|
||||
except Exception as exc:
|
||||
return False, f"无法确认当前圆锥参考半径是否可稳定修改:{exc}"
|
||||
strategy = str(plan.get("resize_strategy") or "")
|
||||
if strategy.startswith("analytic-cone-rebuild") or strategy.startswith("bounded-cone-recut"):
|
||||
return True, ""
|
||||
if strategy.startswith("blocked-complex-cone-reference-radius") or strategy.startswith(
|
||||
"blocked-cone-reference-radius"
|
||||
) or strategy == "radial-affine-scale-cone-reference-radius":
|
||||
message = str(plan.get("message") or "").strip()
|
||||
if message:
|
||||
return False, message
|
||||
return (
|
||||
False,
|
||||
"当前 Face 是圆锥面/拔模面,但不是简单圆锥,也不是可识别的锥孔/沉孔;"
|
||||
"当前版本不把它作为稳定的参考半径/直径参数开放。",
|
||||
)
|
||||
if str(plan.get("status") or "") == "blocked":
|
||||
return False, str(plan.get("message") or "当前圆锥参考半径不能稳定修改。")
|
||||
return True, ""
|
||||
|
||||
def add_spec(
|
||||
*,
|
||||
key: str,
|
||||
@@ -1641,6 +2025,26 @@ 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
|
||||
boundary_vertex_count = _int_or_none(action_info.get("first_level_boundary_vertex_count")) or 0
|
||||
adjacent_face_count = _int_or_none(action_info.get("first_level_adjacent_face_count")) or 0
|
||||
topology_note = str(action_info.get("first_level_topology_note") or "").strip()
|
||||
ignored_note = str(action_info.get("topology_ignored_relation_note") or "").strip()
|
||||
topology_tip = "\n".join(item for item in (topology_note, ignored_note) if item) or (
|
||||
"当前阶段只处理当前 Face、同域碎片、边界 Edge/Vertex 和共享边相邻 Face;二级、三级关系暂不自动传播。"
|
||||
)
|
||||
add_readonly_spec(
|
||||
key="face_first_level_topology",
|
||||
label="一级关系",
|
||||
text=(
|
||||
f"Face 区域 {same_domain_count} 个;边界 Edge {boundary_edge_count} 条;"
|
||||
f"边界 Vertex {boundary_vertex_count} 个;共享边相邻 Face {adjacent_face_count} 个。"
|
||||
),
|
||||
tip=topology_tip,
|
||||
)
|
||||
face_semantics_text = "Face:先改目标值,再用“影响范围”选择改当前面、推拉或调整整个特征。"
|
||||
face_semantics_tip = (
|
||||
"面积是面的大小;面宽/面高是这个面自身平面里的两个方向尺寸;"
|
||||
@@ -2896,33 +3300,49 @@ class WindowStateMixin:
|
||||
current_reference_diameter = (
|
||||
current_reference_radius * 2.0 if current_reference_radius is not None else None
|
||||
)
|
||||
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
|
||||
add_spec(
|
||||
key="cone_reference_radius",
|
||||
label="参考半径(整体)",
|
||||
label="参考半径",
|
||||
current_raw=current_reference_radius if current_reference_radius is not None else "",
|
||||
target_text=numeric_text(current_reference_radius),
|
||||
action="resize_cone_reference_radius",
|
||||
target_attr="cone_reference_radius_input",
|
||||
enabled=current_reference_radius is not None,
|
||||
enabled_tip="输入圆锥面的目标参考半径;程序会围绕圆锥轴径向缩放所属对象。",
|
||||
disabled_tip="当前圆锥面缺少稳定参考半径,不能直接修改。",
|
||||
enabled=reference_radius_enabled,
|
||||
enabled_tip="输入圆锥面的目标参考半径;简单圆锥会解析重建,嵌入式锥孔会优先局部重切。",
|
||||
disabled_tip=(
|
||||
reference_radius_disabled_reason
|
||||
or "当前圆锥面缺少稳定参考半径,不能直接修改。"
|
||||
),
|
||||
value_type="positive",
|
||||
range_hint=f"这不是只替换单个圆锥面的历史参数;同一对象上的其它径向尺寸会跟随变化。 {relative_range_hint(current_reference_radius, 0.25, 0.6)}",
|
||||
range_hint=(
|
||||
"简单圆锥会解析重建;嵌入式锥孔会优先只重切锥孔。"
|
||||
f"复杂圆锥/拔模面暂不使用整体缩放兜底。 {relative_range_hint(current_reference_radius, 0.25, 0.6)}"
|
||||
),
|
||||
used=("reference_radius",),
|
||||
**positive_minimum(),
|
||||
)
|
||||
add_spec(
|
||||
key="cone_reference_diameter",
|
||||
label="参考直径(整体)",
|
||||
label="参考直径",
|
||||
current_raw=current_reference_diameter if current_reference_diameter is not None else "",
|
||||
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,
|
||||
enabled_tip="输入圆锥面的目标参考直径;程序会换算为参考半径后围绕圆锥轴径向缩放所属对象。",
|
||||
disabled_tip="当前圆锥面缺少稳定参考直径,不能直接修改。",
|
||||
enabled=current_reference_diameter is not None and reference_radius_supported,
|
||||
enabled_tip="输入圆锥面的目标参考直径;程序会换算为参考半径后选择解析重建或锥孔局部重切。",
|
||||
disabled_tip=(
|
||||
reference_radius_disabled_reason
|
||||
or "当前圆锥面缺少稳定参考直径,不能直接修改。"
|
||||
),
|
||||
value_type="positive",
|
||||
range_hint=f"这不是只替换单个圆锥面的历史参数;同一对象上的其它径向尺寸会跟随变化。 {relative_range_hint(current_reference_diameter, 0.25, 0.6)}",
|
||||
range_hint=(
|
||||
"简单圆锥会解析重建;嵌入式锥孔会优先只重切锥孔。"
|
||||
f"复杂圆锥/拔模面暂不使用整体缩放兜底。 {relative_range_hint(current_reference_diameter, 0.25, 0.6)}"
|
||||
),
|
||||
target_transform="diameter_to_radius",
|
||||
used=("feature_reference_diameter",),
|
||||
**positive_minimum(),
|
||||
@@ -2931,25 +3351,32 @@ class WindowStateMixin:
|
||||
current_semi_angle_degrees = (
|
||||
abs(math.degrees(current_semi_angle)) if current_semi_angle is not None else None
|
||||
)
|
||||
semi_angle_supported, semi_angle_disabled_reason = cone_semi_angle_capability(current_semi_angle_degrees)
|
||||
semi_angle_enabled = (
|
||||
current_reference_radius is not None
|
||||
and current_semi_angle is not None
|
||||
and semi_angle_supported
|
||||
)
|
||||
add_spec(
|
||||
key="cone_semi_angle_degrees",
|
||||
label="半角(整体)",
|
||||
label="圆锥半角",
|
||||
current_raw=current_semi_angle_degrees if current_semi_angle_degrees is not None else "",
|
||||
target_text=numeric_text(current_semi_angle_degrees),
|
||||
action="resize_cone_reference_radius",
|
||||
action="resize_cone_semi_angle",
|
||||
target_attr="cone_reference_radius_input",
|
||||
enabled=current_reference_radius is not None and current_semi_angle is not None,
|
||||
enabled_tip="输入圆锥面的目标半角,单位是度;程序会换算为目标参考半径后围绕圆锥轴径向缩放所属对象。",
|
||||
disabled_tip="当前圆锥面缺少稳定参考半径或半角,不能直接修改半角。",
|
||||
enabled=semi_angle_enabled,
|
||||
enabled_tip="输入圆锥面的目标半角,单位是度;简单圆锥会解析重建,嵌入式锥孔会优先局部重切。",
|
||||
disabled_tip=(
|
||||
semi_angle_disabled_reason
|
||||
or "当前圆锥面缺少稳定参考半径或半角,不能直接修改半角。"
|
||||
),
|
||||
value_type="positive",
|
||||
range_hint="这是整体径向缩放所属对象,不是只替换单个圆锥面的历史半角参数;建议先小幅修改,当前版本要求半角大于 0 且小于 89 度。",
|
||||
range_hint=(
|
||||
"简单圆锥会解析重建;嵌入式锥孔会优先保持小端半径和深度,只改变锥孔开口。"
|
||||
"复杂圆锥/拔模面暂不使用整体缩放兜底。当前版本要求半角大于 0 且小于 89 度。"
|
||||
),
|
||||
max_value=89.0,
|
||||
max_exclusive=True,
|
||||
target_transform="cone_semi_angle_degrees_to_reference_radius",
|
||||
transform_context={
|
||||
"current_reference_radius": current_reference_radius,
|
||||
"current_semi_angle": current_semi_angle,
|
||||
},
|
||||
used=("semi_angle",),
|
||||
**positive_minimum(),
|
||||
)
|
||||
@@ -3971,6 +4398,31 @@ class WindowStateMixin:
|
||||
row, _spec, _text = changed[0]
|
||||
self.apply_property_row_edit(row)
|
||||
|
||||
def _activate_property_source_feature(self, spec: dict[str, object]) -> None:
|
||||
source_face_id = _int_or_none(spec.get("source_face_id"))
|
||||
if source_face_id is None or source_face_id == self.selected_face_id:
|
||||
return
|
||||
if self.model is None or source_face_id < 0 or source_face_id >= len(self.model.faces):
|
||||
raise ValueError("关联特征已经失效,请重新选择模型对象。")
|
||||
info = spec.get("source_feature_info")
|
||||
if not isinstance(info, dict):
|
||||
info = self._feature_context_info(source_face_id)
|
||||
else:
|
||||
info = dict(info)
|
||||
self.selected_kind = "feature"
|
||||
self.selected_face_id = source_face_id
|
||||
self.selected_edge_id = None
|
||||
self.selected_part_id = int(info.get("part_id", self.model.face_part_ids[source_face_id]))
|
||||
self.selected_solid_id = int(info.get("solid_id", self.model.face_solid_ids[source_face_id]))
|
||||
self.current_info_values = dict(info)
|
||||
self.current_info_text = "\n".join(
|
||||
f"{INFO_LABELS.get(key, key)}: {_format_info_value(key, value)}" for key, value in info.items()
|
||||
)
|
||||
self._sync_id_picker("Feature", source_face_id)
|
||||
if hasattr(self, "_highlight_faces"):
|
||||
self._highlight_faces(_int_values(info.get("feature_highlight_face_ids")) or [source_face_id])
|
||||
self._update_selected_object_title()
|
||||
|
||||
def apply_property_row_edit(self, row: int) -> None:
|
||||
specs = getattr(self, "property_editor_specs", [])
|
||||
if row < 0 or row >= len(specs):
|
||||
@@ -3990,6 +4442,7 @@ class WindowStateMixin:
|
||||
QMessageBox.information(self, "目标值无效", validation_error)
|
||||
return
|
||||
try:
|
||||
self._activate_property_source_feature(spec)
|
||||
if not is_command:
|
||||
self._sync_property_edit_target(spec, text)
|
||||
self._sync_property_preselects(spec)
|
||||
|
||||
Reference in New Issue
Block a user