feat: 推进一级关系参数化编辑与参数导出
This commit is contained in:
@@ -30,6 +30,7 @@ from .geometry_utils import (
|
||||
_tuple_sub,
|
||||
_vector_length,
|
||||
)
|
||||
from .parametric_component import component_name_from_step, default_component_root, export_parametric_component
|
||||
from .ui_helpers import * # noqa: F403
|
||||
from .workers import EditWorker, LoadWorker, ScanWorker
|
||||
|
||||
@@ -122,6 +123,22 @@ def _edit_timing_summary(timings: object, *, limit: int = 5) -> str:
|
||||
return ",".join(parts)
|
||||
|
||||
|
||||
def _start_background_thread(thread: QThread, priority: QThread.Priority = QThread.Priority.LowPriority) -> None:
|
||||
try:
|
||||
thread.start(priority)
|
||||
except TypeError:
|
||||
thread.start()
|
||||
|
||||
|
||||
def _isolated_process_creation_flags() -> int:
|
||||
if sys.platform != "win32":
|
||||
return 0
|
||||
flags = 0
|
||||
for name in ("CREATE_NO_WINDOW", "BELOW_NORMAL_PRIORITY_CLASS"):
|
||||
flags |= int(getattr(subprocess, name, 0))
|
||||
return flags
|
||||
|
||||
|
||||
class WindowActionMixin:
|
||||
def _empty_edge_polydata(self):
|
||||
polydata = vtk.vtkPolyData()
|
||||
@@ -151,11 +168,49 @@ class WindowActionMixin:
|
||||
except OSError as exc:
|
||||
QMessageBox.warning(self, "导出参数失败", f"无法写入 {output_path.name}:{exc}")
|
||||
return
|
||||
self.statusBar().showMessage(f"已导出 {len(rows)} 个输入参数到 {output_path.name}")
|
||||
component_path: Path | None = None
|
||||
component_warning = ""
|
||||
try:
|
||||
component_path = self._export_parametric_component_main(rows)
|
||||
except Exception as exc:
|
||||
component_warning = str(exc)
|
||||
if component_path is not None:
|
||||
self.statusBar().showMessage(f"已导出 {len(rows)} 个输入参数,并生成组件 {component_path.parent.name}")
|
||||
else:
|
||||
self.statusBar().showMessage(f"已导出 {len(rows)} 个输入参数到 {output_path.name}")
|
||||
if hasattr(self, "set_plain_info"):
|
||||
names = "、".join(str(row.get("displayName", "")) for row in rows[:8] if row.get("displayName"))
|
||||
suffix = "……" if len(rows) > 8 else ""
|
||||
self.set_plain_info(f"已导出参数文件:{output_path}\n参数数量:{len(rows)}\n参数:{names}{suffix}")
|
||||
lines = [
|
||||
f"已导出参数文件:{output_path}",
|
||||
f"参数数量:{len(rows)}",
|
||||
f"参数:{names}{suffix}",
|
||||
]
|
||||
if component_path is not None:
|
||||
lines.extend(
|
||||
[
|
||||
f"已生成参数化组件:{component_path.parent}",
|
||||
f"组件入口:{component_path.name}",
|
||||
]
|
||||
)
|
||||
elif component_warning:
|
||||
lines.append(f"组件生成未完成:{component_warning}")
|
||||
self.set_plain_info("\n".join(lines))
|
||||
|
||||
def _export_parametric_component_main(self, rows: list[dict[str, str]]) -> Path:
|
||||
edits = self._selected_parameter_component_edits(rows) if hasattr(self, "_selected_parameter_component_edits") else []
|
||||
if not edits:
|
||||
raise ValueError("当前勾选参数还不能映射为可执行 STEP 编辑操作。")
|
||||
source_step = self.step_path if isinstance(getattr(self, "step_path", None), Path) else None
|
||||
if source_step is None or not source_step.is_file():
|
||||
raise ValueError("当前模型缺少可复用的 STEP 文件路径,请先导入 STEP 模型。")
|
||||
return export_parametric_component(
|
||||
parameters=rows,
|
||||
edits=edits,
|
||||
source_step=source_step,
|
||||
component_root=default_component_root(Path(__file__).resolve().parent.parent),
|
||||
component_name=component_name_from_step(source_step),
|
||||
)
|
||||
|
||||
def export_all(self) -> None:
|
||||
if self.model is None:
|
||||
@@ -488,7 +543,7 @@ class WindowActionMixin:
|
||||
def action():
|
||||
if keep_relations:
|
||||
return self.model.push_pull_face_keep_relations(face_id, distance)
|
||||
return self.model.push_pull_face(face_id, distance)
|
||||
return self.model.push_pull_face(face_id, distance, plan=dict(plan))
|
||||
|
||||
self._run_edit_action(
|
||||
action,
|
||||
@@ -501,8 +556,11 @@ class WindowActionMixin:
|
||||
"distance_rule": "positive=outward fuse, negative=inward cut",
|
||||
"surface": plan.get("surface"),
|
||||
"outward_direction": plan.get("outward_direction"),
|
||||
"plane_direction": plan.get("plane_direction"),
|
||||
"current_plane_position": plan.get("current_plane_position"),
|
||||
"target_plane_position": plan.get("target_plane_position"),
|
||||
"current_plane_center": plan.get("current_plane_center"),
|
||||
"target_plane_center": plan.get("target_plane_center"),
|
||||
"direction_confidence": plan.get("direction_confidence"),
|
||||
"direction_note": plan.get("direction_note"),
|
||||
"resize_strategy": plan.get("resize_strategy"),
|
||||
@@ -1413,7 +1471,11 @@ class WindowActionMixin:
|
||||
height_estimate = _float_or_none(info.get("hole_depth_estimate"))
|
||||
guess = str(info.get("feature_guess", "cylindrical face"))
|
||||
confidence = str(info.get("confidence", "low"))
|
||||
angular_span = _float_or_none(info.get("angular_span"))
|
||||
angular_span = _float_or_none(info.get("same_domain_angular_span"))
|
||||
if angular_span is None:
|
||||
angular_span = _float_or_none(info.get("angular_span"))
|
||||
if bool(info.get("is_full_cylinder")):
|
||||
angular_span = math.tau
|
||||
|
||||
if surface != "cylinder" or current_diameter is None or current_diameter <= 1e-9:
|
||||
blockers.append("当前选中对象不是可识别的圆柱面,不能调整孔/槽直径。")
|
||||
@@ -1588,6 +1650,7 @@ class WindowActionMixin:
|
||||
"中心(局部重建)",
|
||||
"中心(保持关系)",
|
||||
"圆柱高度调整",
|
||||
"圆柱孔径调整",
|
||||
"高度(缩放特征)缩放所属对象",
|
||||
}
|
||||
|
||||
@@ -1603,6 +1666,7 @@ class WindowActionMixin:
|
||||
isolated_geometry_operations = {
|
||||
"push_pull_face",
|
||||
"push_pull_face_keep_relations",
|
||||
"translate_face_plane_offset_owning",
|
||||
"move_face_plane_offset_local",
|
||||
"resize_face_area_local",
|
||||
"resize_face_area",
|
||||
@@ -1615,6 +1679,8 @@ class WindowActionMixin:
|
||||
"resize_shell_thickness_owning_scale",
|
||||
"resize_cylindrical_height",
|
||||
"resize_cylindrical_boss_height",
|
||||
"resize_cylindrical_boss",
|
||||
"move_cylindrical_boss_axis",
|
||||
"resize_cylindrical_height_owning_scale",
|
||||
"resize_cone_reference_radius",
|
||||
"resize_cone_semi_angle",
|
||||
@@ -1832,6 +1898,8 @@ class WindowActionMixin:
|
||||
"does not have",
|
||||
)
|
||||
lower = text.lower()
|
||||
if "target check failed" in lower or "没有到达目标" in text:
|
||||
return "结果未达到目标", "几何内核生成了结果,但目标位置或目标尺寸校验没有通过,模型已回滚。"
|
||||
if any(marker.lower() in lower for marker in unsupported_markers):
|
||||
return "暂未实现", "当前版本暂未实现这类稳定修改。"
|
||||
if any(marker.lower() in lower for marker in risk_markers):
|
||||
@@ -7391,7 +7459,7 @@ class WindowActionMixin:
|
||||
thread.finished.connect(self._forget_scan_thread)
|
||||
self.scan_thread = thread
|
||||
self.scan_worker = worker
|
||||
thread.start()
|
||||
_start_background_thread(thread)
|
||||
|
||||
def _run_scan_task_sync(self, action) -> None:
|
||||
if not self.scan_in_progress:
|
||||
@@ -7745,7 +7813,7 @@ class WindowActionMixin:
|
||||
thread.finished.connect(self._forget_edit_thread)
|
||||
self.edit_thread = thread
|
||||
self.edit_worker = worker
|
||||
thread.start()
|
||||
_start_background_thread(thread)
|
||||
|
||||
def _edit_target_logical_id(self, target_kind: str | None, target_id: int | None) -> int | None:
|
||||
if self.model is None or target_id is None or target_kind not in {"face", "feature"}:
|
||||
@@ -7932,6 +8000,7 @@ class WindowActionMixin:
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
creationflags=_isolated_process_creation_flags(),
|
||||
)
|
||||
self.active_isolated_edit_process = process
|
||||
stdout, stderr = process.communicate(timeout=timeout_seconds)
|
||||
@@ -8107,16 +8176,29 @@ class WindowActionMixin:
|
||||
or _unit_triple_or_none(parameters.get("outward_direction"))
|
||||
)
|
||||
if target_position is not None and plane_direction is not None:
|
||||
part_id = self._edit_integrity_int_or_none(parameters.get("part_id"))
|
||||
solid_id = self._edit_integrity_int_or_none(parameters.get("solid_id"))
|
||||
candidate_ids.extend(
|
||||
self._face_ids_at_plane_position(
|
||||
model,
|
||||
target_position,
|
||||
plane_direction,
|
||||
_float_or_none(parameters.get("bbox_diagonal")),
|
||||
self._edit_integrity_int_or_none(parameters.get("part_id")),
|
||||
self._edit_integrity_int_or_none(parameters.get("solid_id")),
|
||||
part_id,
|
||||
solid_id,
|
||||
)
|
||||
)
|
||||
if solid_id is not None and solid_id >= 0:
|
||||
candidate_ids.extend(
|
||||
self._face_ids_at_plane_position(
|
||||
model,
|
||||
target_position,
|
||||
plane_direction,
|
||||
_float_or_none(parameters.get("bbox_diagonal")),
|
||||
part_id,
|
||||
None,
|
||||
)
|
||||
)
|
||||
for candidate_id in candidate_ids:
|
||||
if not (0 <= int(candidate_id) < len(model.faces)):
|
||||
continue
|
||||
@@ -8470,6 +8552,10 @@ class WindowActionMixin:
|
||||
for face_id in filtered(range(len(model.faces))):
|
||||
if face_id not in result:
|
||||
result.append(face_id)
|
||||
if solid_id is not None and solid_id >= 0:
|
||||
for face_id in filtered(range(len(model.faces)), require_solid=False):
|
||||
if face_id not in result:
|
||||
result.append(face_id)
|
||||
return result
|
||||
if surface in {"plane", "cylinder", "cone", "sphere", "torus"} and has_target_value:
|
||||
result = filtered(range(len(model.faces)))
|
||||
@@ -8921,6 +9007,8 @@ class WindowActionMixin:
|
||||
if self.selected_kind is None:
|
||||
timing_detail = f"\n\n性能耗时:{timing_text}" if timing_text else ""
|
||||
self.set_plain_info(f"{record.detail}{timing_detail}\n\n{locator_note}")
|
||||
if hasattr(self, "_after_property_edit_finished"):
|
||||
self._after_property_edit_finished(success=True)
|
||||
|
||||
@Slot(str)
|
||||
def _fail_edit_action(self, message: str) -> None:
|
||||
@@ -8940,6 +9028,8 @@ class WindowActionMixin:
|
||||
self.statusBar().showMessage(
|
||||
"后台编辑已取消,模型已保持在编辑前状态" if close_after_cancel else "不能修改,模型未修改"
|
||||
)
|
||||
if hasattr(self, "_after_property_edit_finished"):
|
||||
self._after_property_edit_finished(success=False)
|
||||
if close_after_cancel:
|
||||
if self.edit_thread is not None and self.edit_thread.isRunning():
|
||||
self.edit_thread.quit()
|
||||
|
||||
Reference in New Issue
Block a user