feat: 完善 Face 参数化编辑和隔离执行
This commit is contained in:
+30
-16
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
import faulthandler
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -78,6 +79,12 @@ def _enable_crash_log() -> None:
|
||||
faulthandler.enable(all_threads=True)
|
||||
|
||||
|
||||
def _crash_log_requested(argv: list[str]) -> bool:
|
||||
if "--crash-log" in argv:
|
||||
return True
|
||||
return os.environ.get("STEP_EDITOR_CRASH_LOG", "").strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, InfoPanelMixin, QMainWindow):
|
||||
ui_task_requested = Signal(object)
|
||||
|
||||
@@ -334,9 +341,9 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
background: #fff8ef;
|
||||
border: 1px solid #edc98e;
|
||||
border-left: 5px solid #d97706;
|
||||
padding-left: 5px;
|
||||
padding-right: 3px;
|
||||
padding-bottom: 7px;
|
||||
padding-left: 2px;
|
||||
padding-right: 1px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
QGroupBox#editSection::title {
|
||||
background: #fff0d8;
|
||||
@@ -423,7 +430,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
min-height: 18px;
|
||||
padding: 1px 6px;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
QPushButton#propertyRowEditButton:hover {
|
||||
background: #f97316;
|
||||
@@ -434,7 +441,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
border-color: #7c2d12;
|
||||
color: #ffffff;
|
||||
padding-top: 2px;
|
||||
padding-left: 7px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
QPushButton#propertyRowEditButton[changed="true"] {
|
||||
background: #dcfce7;
|
||||
@@ -450,13 +457,19 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
border: 1px dashed #bcc7d4;
|
||||
color: #8f99a8;
|
||||
}
|
||||
QPushButton#propertyRowEditButton[invalid="true"],
|
||||
QPushButton#propertyRowEditButton[invalid="true"]:disabled {
|
||||
background: #fee2e2;
|
||||
border: 1px solid #ef4444;
|
||||
color: #991b1b;
|
||||
}
|
||||
QComboBox#propertyScopeCombo {
|
||||
background: #ffffff;
|
||||
border: 1px solid #f59e0b;
|
||||
border-radius: 5px;
|
||||
color: #7c2d12;
|
||||
min-height: 18px;
|
||||
padding: 0px 4px;
|
||||
padding: 0px 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QComboBox#propertyScopeCombo:hover {
|
||||
@@ -478,7 +491,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
border-radius: 5px;
|
||||
color: #111827;
|
||||
min-height: 18px;
|
||||
padding: 0px 5px;
|
||||
padding: 0px 2px;
|
||||
selection-background-color: #bfdbfe;
|
||||
}
|
||||
QLineEdit#propertyTargetEditor:hover {
|
||||
@@ -488,7 +501,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
QLineEdit#propertyTargetEditor:focus {
|
||||
background: #ffffff;
|
||||
border: 2px solid #2563eb;
|
||||
padding: 0px 4px;
|
||||
padding: 0px 1px;
|
||||
}
|
||||
QPushButton#propertyExpandBar {
|
||||
background: #f8fafc;
|
||||
@@ -568,12 +581,12 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
padding: 0;
|
||||
}
|
||||
QTableWidget#propertyTable::item {
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
}
|
||||
QTableWidget#propertyTable QHeaderView::section {
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
}
|
||||
QTabWidget::pane {
|
||||
border: 1px solid #d8e0eb;
|
||||
@@ -822,7 +835,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
"选择模型对象后,这里会列出当前对象的属性和值;能改的行可以输入目标值,不能改的行只读。",
|
||||
)
|
||||
object_edit_layout = QVBoxLayout(self.object_edit_box)
|
||||
object_edit_layout.setContentsMargins(2, 8, 2, 4)
|
||||
object_edit_layout.setContentsMargins(0, 8, 0, 4)
|
||||
object_edit_layout.setSpacing(4)
|
||||
self.property_table = QTableWidget(0, 5)
|
||||
self.property_table.setObjectName("propertyTable")
|
||||
@@ -984,10 +997,10 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
help_tip(self.face_area_input, "目标面面积。当前选中对象表会使用这个隐藏输入执行所属特征或 Solid 的均匀缩放。")
|
||||
self.face_width_input = QLineEdit("", edit_box)
|
||||
self.face_width_input.setVisible(False)
|
||||
help_tip(self.face_width_input, "目标面宽。当前选中对象表会使用这个隐藏输入只修改当前 Face 的平面内宽度。")
|
||||
help_tip(self.face_width_input, "目标面宽。当前选中对象表会使用这个隐藏输入修改 Face 平面内第一个方向的尺寸。")
|
||||
self.face_height_input = QLineEdit("", edit_box)
|
||||
self.face_height_input.setVisible(False)
|
||||
help_tip(self.face_height_input, "目标面高。当前选中对象表会使用这个隐藏输入只修改当前 Face 的平面内高度。")
|
||||
help_tip(self.face_height_input, "目标面高。当前选中对象表会使用这个隐藏输入修改 Face 平面内第二个方向的尺寸。")
|
||||
self.resize_boss_button = QPushButton("调整圆柱凸台直径")
|
||||
help_tip(self.resize_boss_button, "修改完整圆柱凸台直径。变大会加料,变小会重建凸台区域。")
|
||||
self.resize_boss_button.clicked.connect(self.resize_boss)
|
||||
@@ -1375,7 +1388,8 @@ def _parse_args(argv: list[str]) -> tuple[Path, bool]:
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_enable_crash_log()
|
||||
if _crash_log_requested(sys.argv):
|
||||
_enable_crash_log()
|
||||
path, smoke_test = _parse_args(sys.argv)
|
||||
app = QApplication(sys.argv)
|
||||
window = StepEditorWindow(path, background_load=not smoke_test)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
from .model import StepModel
|
||||
|
||||
|
||||
def _execute(model: StepModel, operation: str, args: list[object]) -> str:
|
||||
if operation == "push_pull_face":
|
||||
return model.push_pull_face(int(args[0]), float(args[1]))
|
||||
if operation == "move_face_plane_offset_local":
|
||||
return model.move_face_plane_offset_local(int(args[0]), float(args[1]))
|
||||
if operation == "resize_face_area_local":
|
||||
return model.resize_face_area_local(int(args[0]), float(args[1]))
|
||||
if operation == "resize_face_area":
|
||||
return model.resize_face_area(int(args[0]), float(args[1]))
|
||||
if operation == "resize_face_size_local":
|
||||
return model.resize_face_size_local(int(args[0]), float(args[1]), str(args[2]))
|
||||
if operation == "resize_face_size_owning_scale":
|
||||
return model.resize_face_size_owning_scale(int(args[0]), float(args[1]), str(args[2]))
|
||||
if operation == "move_face_center_local":
|
||||
center = list(args[1])
|
||||
if len(center) != 3:
|
||||
raise ValueError("move_face_center_local requires a 3D target center.")
|
||||
return model.move_face_center_local(
|
||||
int(args[0]),
|
||||
(float(center[0]), float(center[1]), float(center[2])),
|
||||
)
|
||||
if operation == "resize_shell_thickness":
|
||||
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]))
|
||||
raise ValueError(f"Unsupported isolated edit operation: {operation}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Run one high-risk geometry edit in an isolated process.")
|
||||
parser.add_argument("request", help="JSON request file.")
|
||||
parsed = parser.parse_args()
|
||||
|
||||
request_path = Path(parsed.request)
|
||||
response_path = request_path.with_suffix(".response.json")
|
||||
try:
|
||||
request = json.loads(request_path.read_text(encoding="utf-8-sig"))
|
||||
input_path = Path(str(request["input_path"]))
|
||||
output_path = Path(str(request["output_path"]))
|
||||
operation = str(request["operation"])
|
||||
args = list(request.get("args") or [])
|
||||
|
||||
model = StepModel.load(input_path)
|
||||
message = _execute(model, operation, args)
|
||||
model.export_all(output_path)
|
||||
response_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": True,
|
||||
"message": message,
|
||||
"stats": model.stats().__dict__,
|
||||
"output_path": str(output_path),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return 0
|
||||
except Exception as exc:
|
||||
response_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": False,
|
||||
"error": str(exc),
|
||||
"traceback": traceback.format_exc(),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+156
-2
@@ -9,7 +9,12 @@ from OCC.Core.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
|
||||
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Defeaturing, BRepAlgoAPI_Fuse
|
||||
from OCC.Core.BRepBndLib import brepbndlib
|
||||
from OCC.Core.BOPAlgo import BOPAlgo_GlueFull
|
||||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeVertex, BRepBuilderAPI_Transform
|
||||
from OCC.Core.BRepBuilderAPI import (
|
||||
BRepBuilderAPI_MakeFace,
|
||||
BRepBuilderAPI_MakeVertex,
|
||||
BRepBuilderAPI_MakeWire,
|
||||
BRepBuilderAPI_Transform,
|
||||
)
|
||||
from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape
|
||||
from OCC.Core.BRepCheck import BRepCheck_Analyzer
|
||||
from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier
|
||||
@@ -58,6 +63,7 @@ from OCC.Core.TopAbs import (
|
||||
TopAbs_OUT,
|
||||
TopAbs_REVERSED,
|
||||
TopAbs_SOLID,
|
||||
TopAbs_WIRE,
|
||||
)
|
||||
from OCC.Core.TopExp import TopExp_Explorer, topexp
|
||||
from OCC.Core.TopLoc import TopLoc_Location
|
||||
@@ -104,6 +110,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_edge_ids_cache: dict[int, list[int]] = {}
|
||||
self._edge_face_ids_cache: dict[int, list[int]] = {}
|
||||
self._same_domain_face_ids_cache: dict[int, list[int]] = {}
|
||||
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
|
||||
self._same_domain_duplicate_edge_ids_cache: set[int] | None = None
|
||||
@@ -184,6 +191,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_edge_ids_cache.clear()
|
||||
self._edge_face_ids_cache.clear()
|
||||
self._same_domain_face_ids_cache.clear()
|
||||
self._local_face_deform_readiness_cache.clear()
|
||||
self._edge_duplicate_key_ids_cache = None
|
||||
self._same_domain_internal_edge_ids_cache = None
|
||||
self._same_domain_duplicate_edge_ids_cache = None
|
||||
@@ -275,6 +283,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_info_cache.clear()
|
||||
self._feature_info_cache.clear()
|
||||
self._same_domain_face_ids_cache.clear()
|
||||
self._local_face_deform_readiness_cache.clear()
|
||||
|
||||
def quick_face_info(self, face_id: int) -> dict[str, object]:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
@@ -310,6 +319,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"selection_info_note": "快速选择信息;同域面、端盖、底面等深层识别会在执行编辑计划或手动扫描时再计算。",
|
||||
}
|
||||
info.update(_shape_bounds_info(face))
|
||||
info.update(self._face_boundary_wire_info(face))
|
||||
if surface_type == GeomAbs_Plane:
|
||||
plane = surf.Plane()
|
||||
direction = plane.Axis().Direction()
|
||||
@@ -328,6 +338,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
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))
|
||||
info.update(
|
||||
self._local_face_plane_size_info(
|
||||
face_id,
|
||||
@@ -420,6 +431,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"boundary_edges": boundary_edges,
|
||||
}
|
||||
info.update(_shape_bounds_info(face))
|
||||
info.update(self._face_boundary_wire_info(face))
|
||||
if surface_type == GeomAbs_Plane:
|
||||
plane = surf.Plane()
|
||||
direction = plane.Axis().Direction()
|
||||
@@ -433,6 +445,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
info["push_pull_minus_side"] = push_pull_direction["minus_side_state"]
|
||||
info["push_pull_confidence"] = push_pull_direction["confidence"]
|
||||
info["push_pull_note"] = push_pull_direction["note"]
|
||||
info.update(self._local_face_deform_readiness(face_id))
|
||||
info.update(
|
||||
self._local_face_plane_size_info(
|
||||
face_id,
|
||||
@@ -1128,6 +1141,112 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_edge_ids_cache[face_id] = list(edge_ids)
|
||||
return edge_ids
|
||||
|
||||
def _face_boundary_wire_info(self, face: TopoDS_Shape) -> dict[str, object]:
|
||||
try:
|
||||
boundary_wires = len(_explore(face, TopAbs_WIRE))
|
||||
except Exception:
|
||||
boundary_wires = 0
|
||||
inner_boundary_wires = max(boundary_wires - 1, 0)
|
||||
return {
|
||||
"boundary_wires": boundary_wires,
|
||||
"inner_boundary_wires": inner_boundary_wires,
|
||||
"has_inner_boundaries": inner_boundary_wires > 0,
|
||||
}
|
||||
|
||||
def _local_face_deform_readiness(self, face_id: int) -> dict[str, object]:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
return {
|
||||
"local_face_deform_ready": False,
|
||||
"local_face_deform_blocker": "Face ID 不存在,不能做局部 Face 变形。",
|
||||
}
|
||||
solid_id = self.face_solid_ids[face_id] if face_id < len(self.face_solid_ids) else -1
|
||||
cache_key = solid_id if solid_id >= 0 else -(face_id + 1)
|
||||
cached = self._local_face_deform_readiness_cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return dict(cached)
|
||||
|
||||
def remember(info: dict[str, object]) -> dict[str, object]:
|
||||
self._local_face_deform_readiness_cache[cache_key] = dict(info)
|
||||
return dict(info)
|
||||
|
||||
if solid_id < 0 or solid_id >= len(self.solids):
|
||||
return remember(
|
||||
{
|
||||
"local_face_deform_ready": False,
|
||||
"local_face_deform_face_count": 1,
|
||||
"local_face_deform_blocker": "找不到当前 Face 所属 Solid,不能做“只改当前面”的局部重建。",
|
||||
}
|
||||
)
|
||||
solid_face_ids = [index for index, item in enumerate(self.face_solid_ids) if item == solid_id]
|
||||
if not solid_face_ids:
|
||||
solid_face_ids = [face_id]
|
||||
face_count = len(solid_face_ids)
|
||||
if face_count > 128:
|
||||
return remember(
|
||||
{
|
||||
"local_face_deform_ready": False,
|
||||
"local_face_deform_face_count": face_count,
|
||||
"local_face_deform_blocker": "所属 Solid 的 Face 数量超过 128,当前版本不开放“只改当前面”的局部重建。",
|
||||
}
|
||||
)
|
||||
|
||||
source_shape = self.solids[solid_id][1] if 0 <= solid_id < len(self.solids) else self.faces[face_id]
|
||||
tolerance = max(_shape_diagonal(source_shape) * 1e-7, 1e-6)
|
||||
for item in solid_face_ids:
|
||||
face = self.faces[item]
|
||||
try:
|
||||
surface = BRepAdaptor_Surface(face)
|
||||
except Exception:
|
||||
return remember(
|
||||
{
|
||||
"local_face_deform_ready": False,
|
||||
"local_face_deform_face_count": face_count,
|
||||
"local_face_deform_blocker": "所属 Solid 里有 Face 不能稳定读取曲面类型,不能做局部 Face 变形。",
|
||||
}
|
||||
)
|
||||
if surface.GetType() != GeomAbs_Plane:
|
||||
return remember(
|
||||
{
|
||||
"local_face_deform_ready": False,
|
||||
"local_face_deform_face_count": face_count,
|
||||
"local_face_deform_blocker": "所属 Solid 含有曲面,当前版本只对简单全平面 Solid 开放“只改当前面”。",
|
||||
}
|
||||
)
|
||||
wire_info = self._face_boundary_wire_info(face)
|
||||
if bool(wire_info.get("has_inner_boundaries")):
|
||||
return remember(
|
||||
{
|
||||
"local_face_deform_ready": False,
|
||||
"local_face_deform_face_count": face_count,
|
||||
"local_face_deform_blocker": "所属 Solid 里有带内孔/内边界的 Face,请优先使用孔、槽或推拉等专门修改方式。",
|
||||
}
|
||||
)
|
||||
try:
|
||||
if len(self._local_deform_face_vertex_points(face, tolerance)) < 3:
|
||||
return remember(
|
||||
{
|
||||
"local_face_deform_ready": False,
|
||||
"local_face_deform_face_count": face_count,
|
||||
"local_face_deform_blocker": "所属 Solid 里有 Face 顶点环不能稳定读取,不能做局部 Face 变形。",
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
return remember(
|
||||
{
|
||||
"local_face_deform_ready": False,
|
||||
"local_face_deform_face_count": face_count,
|
||||
"local_face_deform_blocker": "所属 Solid 里有 Face 顶点环读取失败,不能做局部 Face 变形。",
|
||||
}
|
||||
)
|
||||
|
||||
return remember(
|
||||
{
|
||||
"local_face_deform_ready": True,
|
||||
"local_face_deform_face_count": face_count,
|
||||
"local_face_deform_blocker": "",
|
||||
}
|
||||
)
|
||||
|
||||
def face_boundary_edge_ids(self, face_id: int) -> list[int]:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
return []
|
||||
@@ -1166,6 +1285,22 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_info_cache.pop(face_id, None)
|
||||
self._feature_info_cache.pop(face_id, None)
|
||||
|
||||
def assign_logical_face_region_exclusive(self, logical_id: int, face_ids: Iterable[int]) -> None:
|
||||
valid_face_ids = sorted({int(face_id) for face_id in face_ids if 0 <= int(face_id) < len(self.faces)})
|
||||
if not valid_face_ids:
|
||||
return
|
||||
logical_id = int(logical_id)
|
||||
replacement_id = max([logical_id, len(self.faces), *[int(item) for item in self.face_logical_ids]], default=logical_id) + 1
|
||||
for face_id, current_logical_id in enumerate(list(self.face_logical_ids)):
|
||||
if int(current_logical_id) != logical_id or face_id in valid_face_ids:
|
||||
continue
|
||||
self.face_logical_ids[face_id] = replacement_id
|
||||
replacement_id += 1
|
||||
self._quick_face_info_cache.pop(face_id, None)
|
||||
self._face_info_cache.pop(face_id, None)
|
||||
self._feature_info_cache.pop(face_id, None)
|
||||
self.assign_logical_face_region(logical_id, valid_face_ids)
|
||||
|
||||
def nearest_edge_id_to_point(
|
||||
self,
|
||||
edge_ids: Iterable[int],
|
||||
@@ -1487,7 +1622,26 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
return sorted(edge_id for edge_id, count in counts.items() if count == 1)
|
||||
|
||||
def _push_pull_profile_shape(self, face_ids: Iterable[int]) -> TopoDS_Shape:
|
||||
profile_faces = [self.faces[face_id] for face_id in face_ids if 0 <= face_id < len(self.faces)]
|
||||
valid_face_ids = [int(face_id) for face_id in face_ids if 0 <= int(face_id) < len(self.faces)]
|
||||
if not valid_face_ids:
|
||||
raise ValueError("No planar faces were found for push/pull.")
|
||||
if len(valid_face_ids) > 1:
|
||||
boundary_edge_ids = self._region_boundary_edge_ids(valid_face_ids)
|
||||
if boundary_edge_ids:
|
||||
try:
|
||||
wire = BRepBuilderAPI_MakeWire()
|
||||
for edge_id in boundary_edge_ids:
|
||||
wire.Add(topods.Edge(self.edges[edge_id]))
|
||||
if not hasattr(wire, "IsDone") or wire.IsDone():
|
||||
face_builder = BRepBuilderAPI_MakeFace(wire.Wire())
|
||||
if not hasattr(face_builder, "IsDone") or face_builder.IsDone():
|
||||
profile = face_builder.Face()
|
||||
if not profile.IsNull():
|
||||
_ensure_valid_shape(profile)
|
||||
return profile
|
||||
except Exception:
|
||||
pass
|
||||
profile_faces = [self.faces[face_id] for face_id in valid_face_ids]
|
||||
if not profile_faces:
|
||||
raise ValueError("No planar faces were found for push/pull.")
|
||||
return _unify_same_domain_shape(_compound_from_shapes(profile_faces))
|
||||
|
||||
+1015
-8
File diff suppressed because it is too large
Load Diff
+12
-12
@@ -483,22 +483,22 @@ INFO_LABELS = {
|
||||
"area_center": "面积中心",
|
||||
"local_face_width": "当前面宽",
|
||||
"local_face_height": "当前面高",
|
||||
"local_face_width_direction": "面宽方向",
|
||||
"local_face_height_direction": "面高方向",
|
||||
"local_face_size_center": "面宽/面高中心",
|
||||
"face_size_axis": "Face尺寸方向",
|
||||
"face_size_label": "Face尺寸名称",
|
||||
"local_face_width_direction": "面宽 方向",
|
||||
"local_face_height_direction": "面高 方向",
|
||||
"local_face_size_center": "面宽/面高 中心",
|
||||
"face_size_axis": "面宽/面高 方向",
|
||||
"face_size_label": "面宽/面高 名称",
|
||||
"current_face_width": "当前面宽",
|
||||
"target_face_width": "目标面宽",
|
||||
"current_face_height": "当前面高",
|
||||
"target_face_height": "目标面高",
|
||||
"current_face_size": "当前Face尺寸",
|
||||
"target_face_size": "目标Face尺寸",
|
||||
"face_size_delta": "Face尺寸变化量",
|
||||
"face_size_delta_ratio": "Face尺寸变化比例",
|
||||
"face_size_scale": "Face尺寸缩放比例",
|
||||
"face_size_center": "Face尺寸缩放中心",
|
||||
"face_size_axis_direction": "Face尺寸缩放方向",
|
||||
"current_face_size": "当前面宽/面高",
|
||||
"target_face_size": "目标面宽/面高",
|
||||
"face_size_delta": "面宽/面高 变化量",
|
||||
"face_size_delta_ratio": "面宽/面高 变化比例",
|
||||
"face_size_scale": "面宽/面高 缩放比例",
|
||||
"face_size_center": "面宽/面高 缩放中心",
|
||||
"face_size_axis_direction": "面宽/面高 缩放方向",
|
||||
"owning_face_size_rebuild_mode": "所属对象尺寸重建模式",
|
||||
"length_center": "长度中心",
|
||||
"bbox_min": "包围盒最小点",
|
||||
|
||||
+361
-81
@@ -1,8 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import vtk
|
||||
from PySide6.QtCore import Qt, QThread, Slot
|
||||
@@ -336,6 +340,7 @@ class WindowActionMixin:
|
||||
return
|
||||
face_id = self.selected_face_id
|
||||
plan = self._quick_push_pull_plan(face_id, distance)
|
||||
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能推拉平面", str(plan["message"]))
|
||||
self.statusBar().showMessage("推拉平面已阻止")
|
||||
@@ -343,6 +348,11 @@ class WindowActionMixin:
|
||||
if plan["risk"] != "low":
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||||
isolation_line = (
|
||||
"本次会在隔离子进程里执行高风险 OCC 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||||
if str(plan.get("risk")) == "high"
|
||||
else ""
|
||||
)
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"确认推拉平面",
|
||||
@@ -356,6 +366,7 @@ class WindowActionMixin:
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
f"{isolation_line}"
|
||||
"继续操作会修改当前 B-Rep 结果几何,并支持失败回滚/撤销。确定继续吗?"
|
||||
),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
@@ -393,10 +404,13 @@ class WindowActionMixin:
|
||||
"push_pull_scope_face_ids": plan.get("push_pull_scope_face_ids"),
|
||||
"push_pull_scope_face_count": plan.get("push_pull_scope_face_count"),
|
||||
"push_pull_scope_note": plan.get("push_pull_scope_note"),
|
||||
"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"),
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=self._isolation_for_plan(plan, "push_pull_face", [face_id, distance]),
|
||||
)
|
||||
|
||||
def _quick_push_pull_plan(self, face_id: int, distance: float) -> dict[str, object]:
|
||||
@@ -432,6 +446,7 @@ class WindowActionMixin:
|
||||
risk = "low"
|
||||
status = "ready"
|
||||
warnings: list[str] = []
|
||||
blockers: list[str] = []
|
||||
if distance_abs <= 1e-9:
|
||||
return {
|
||||
"status": "blocked",
|
||||
@@ -451,6 +466,31 @@ class WindowActionMixin:
|
||||
status = "caution"
|
||||
warnings.append("面移动距离相对当前面尺寸偏大,请确认预览范围。")
|
||||
|
||||
outward_tuple = tuple(float(item) for item in outward)
|
||||
inward_material_depth = None
|
||||
inward_cut_ratio = None
|
||||
if distance < 0 and self.model is not None:
|
||||
inward_material_depth = self.model._push_pull_inward_material_depth(face_id, outward_tuple)
|
||||
if inward_material_depth is not None and inward_material_depth > 1e-9:
|
||||
inward_cut_ratio = distance_abs / inward_material_depth
|
||||
depth_tolerance = max(
|
||||
inward_material_depth * 1e-5,
|
||||
(bbox_diagonal or 0.0) * 1e-7,
|
||||
1e-6,
|
||||
)
|
||||
if distance_abs >= inward_material_depth - depth_tolerance:
|
||||
status = "blocked"
|
||||
risk = "blocked"
|
||||
blockers.append("向内切削距离达到或超过当前面背后的材料厚度;继续执行很可能把实体切空或生成无效几何。")
|
||||
elif inward_cut_ratio >= 0.85:
|
||||
risk = "high"
|
||||
warnings.append("向内切削距离已经接近当前面背后的材料厚度,剩余壁厚很薄,请谨慎确认。")
|
||||
elif inward_cut_ratio >= 0.6 and risk != "high":
|
||||
risk = "medium"
|
||||
warnings.append("向内切削距离超过当前面背后材料厚度的 60%,请确认不会切穿。")
|
||||
if status != "blocked" and risk in {"medium", "high"}:
|
||||
status = "caution"
|
||||
|
||||
scope_face_ids = (
|
||||
_int_values(info.get("push_pull_scope_face_ids"))
|
||||
or _int_values(info.get("feature_highlight_face_ids"))
|
||||
@@ -476,12 +516,14 @@ class WindowActionMixin:
|
||||
if solid_id is None and self.model is not None and 0 <= face_id < len(self.model.face_solid_ids):
|
||||
solid_id = self.model.face_solid_ids[face_id]
|
||||
message = " ".join(warnings) if warnings else "可以尝试偏移该平面;完整几何检查会在后台执行。"
|
||||
if blockers:
|
||||
message = " ".join(blockers + warnings)
|
||||
return {
|
||||
"status": status,
|
||||
"risk": risk,
|
||||
"message": message,
|
||||
"warnings": ";".join(warnings),
|
||||
"blockers": "",
|
||||
"blockers": ";".join(blockers),
|
||||
"face_id": face_id,
|
||||
"part_id": part_id,
|
||||
"solid_id": solid_id,
|
||||
@@ -489,7 +531,9 @@ class WindowActionMixin:
|
||||
"surface": surface,
|
||||
"area": info.get("area"),
|
||||
"bbox_diagonal": bbox_diagonal,
|
||||
"outward_direction": tuple(float(item) for item in outward),
|
||||
"push_pull_inward_material_depth": inward_material_depth,
|
||||
"push_pull_inward_cut_ratio": inward_cut_ratio,
|
||||
"outward_direction": outward_tuple,
|
||||
"current_plane_position": current_plane_position,
|
||||
"target_plane_position": target_plane_position,
|
||||
"resize_strategy": "push-pull-planar-face-region",
|
||||
@@ -547,7 +591,12 @@ class WindowActionMixin:
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
"当前版本会移动当前平面区域,让它和相对平面的距离接近目标厚度;这不是 CAD 壳命令参数编辑。确定继续吗?"
|
||||
+ (
|
||||
"本次会在隔离子进程里执行高风险 OCC 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||||
if str(plan.get("risk")) == "high"
|
||||
else ""
|
||||
)
|
||||
+ "当前版本会移动当前平面区域,让它和相对平面的距离接近目标厚度;这不是 CAD 壳命令参数编辑。确定继续吗?"
|
||||
),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
@@ -556,7 +605,10 @@ class WindowActionMixin:
|
||||
self.statusBar().showMessage("已取消薄壁厚度调整")
|
||||
return
|
||||
|
||||
self._show_shell_thickness_preview(face_id, target_thickness)
|
||||
if str(plan.get("risk")) == "high":
|
||||
self.clear_edit_preview(render=False)
|
||||
else:
|
||||
self._show_shell_thickness_preview(face_id, target_thickness)
|
||||
|
||||
def action():
|
||||
return self.model.resize_shell_thickness(face_id, target_thickness)
|
||||
@@ -595,6 +647,7 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="feature" if self.selected_kind == "feature" else "face",
|
||||
target_id=face_id,
|
||||
isolation=self._isolation_for_plan(plan, "resize_shell_thickness", [face_id, target_thickness]),
|
||||
)
|
||||
|
||||
def resize_shell_thickness_owning_scale(self) -> None:
|
||||
@@ -627,6 +680,9 @@ class WindowActionMixin:
|
||||
f"缩放比例: {_format_value(plan.get('affine_scale'))}",
|
||||
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}",
|
||||
f"厚度方向: {_format_value(plan.get('affine_axis_direction'))}",
|
||||
f"重建模式: {_format_value(plan.get('owning_shell_thickness_rebuild_mode'))}",
|
||||
f"重建Face数: {_format_value(plan.get('local_face_deform_face_count'))}",
|
||||
f"移动顶点数: {_format_value(plan.get('local_face_deform_moved_point_count'))}",
|
||||
f"相对平面 Face: {_format_value(plan.get('shell_opposite_face_id', ''))}",
|
||||
"当前语义: 沿薄壁/壳体厚度方向整体缩放所属特征或 Solid;不是推拉当前平面区域。",
|
||||
]
|
||||
@@ -668,6 +724,9 @@ class WindowActionMixin:
|
||||
"affine_axis_point": plan.get("affine_axis_point"),
|
||||
"affine_axis_direction": plan.get("affine_axis_direction"),
|
||||
"affine_target_kind": plan.get("affine_target_kind"),
|
||||
"owning_shell_thickness_rebuild_mode": plan.get("owning_shell_thickness_rebuild_mode"),
|
||||
"local_face_deform_face_count": plan.get("local_face_deform_face_count"),
|
||||
"local_face_deform_moved_point_count": plan.get("local_face_deform_moved_point_count"),
|
||||
"part_solid_count": plan.get("part_solid_count"),
|
||||
"resize_strategy": plan.get("resize_strategy"),
|
||||
"edit_strategy_label": plan.get("edit_strategy_label"),
|
||||
@@ -681,6 +740,7 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=self._isolation_for_plan(plan, "resize_shell_thickness_owning_scale", [face_id, target_thickness]),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -860,7 +920,10 @@ class WindowActionMixin:
|
||||
QMessageBox.information(self, f"不能{title}", str(plan.get("message", "")))
|
||||
self.statusBar().showMessage(blocked_status)
|
||||
return False
|
||||
if self._block_unisolated_high_risk_operation(f"已阻止高风险{title}", plan):
|
||||
if (
|
||||
not self._quick_edit_title_supports_isolation(title)
|
||||
and self._block_unisolated_high_risk_operation(f"已阻止高风险{title}", plan)
|
||||
):
|
||||
return False
|
||||
if plan.get("risk") == "low":
|
||||
return True
|
||||
@@ -883,7 +946,12 @@ class WindowActionMixin:
|
||||
+ f"\n风险: {plan.get('risk')}\n"
|
||||
+ warnings_line
|
||||
+ f"\n{plan.get('message', '')}\n\n"
|
||||
"为避免复杂 STEP 在界面线程卡死,本次不会先生成红/绿预览;"
|
||||
+ (
|
||||
"本次会在隔离子进程里执行高风险 OCC 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||||
if str(plan.get("risk")) == "high" and self._quick_edit_title_supports_isolation(title)
|
||||
else ""
|
||||
)
|
||||
+ "为避免复杂 STEP 在界面线程卡死,本次不会先生成红/绿预览;"
|
||||
"详细几何方案会进入后台计算,完成后自动刷新模型。\n\n"
|
||||
"确定继续吗?"
|
||||
),
|
||||
@@ -895,6 +963,36 @@ class WindowActionMixin:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _quick_edit_title_supports_isolation(self, title: str) -> bool:
|
||||
return title in {
|
||||
"薄壁厚度(整体)缩放所属对象",
|
||||
"只缩放当前Face面积",
|
||||
"面面积(整体)",
|
||||
"面宽(当前面)",
|
||||
"面高(当前面)",
|
||||
"面宽(整体)",
|
||||
"面高(整体)",
|
||||
"面偏移(当前面)",
|
||||
"只移动当前Face",
|
||||
}
|
||||
|
||||
def _isolation_for_plan(
|
||||
self,
|
||||
plan: dict[str, object],
|
||||
operation: str,
|
||||
args: list[object],
|
||||
*,
|
||||
timeout_seconds: float = 180.0,
|
||||
) -> dict[str, object] | None:
|
||||
if str(plan.get("risk", "")) != "high":
|
||||
return None
|
||||
return {
|
||||
"operation": operation,
|
||||
"args": args,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
"reason": "high-risk-occ-edit",
|
||||
}
|
||||
|
||||
def resize_hole(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
@@ -3776,6 +3874,90 @@ class WindowActionMixin:
|
||||
def move_edge_end_point(self) -> None:
|
||||
self._move_edge_endpoint("end")
|
||||
|
||||
def move_circular_edge_axis_center(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
if self._edit_busy("请等待当前编辑完成后再移动圆Edge相邻轴心。"):
|
||||
return
|
||||
if self.selected_edge_id is None:
|
||||
QMessageBox.information(self, "未选择圆Edge", "请先选择一条圆形或圆弧 Edge。")
|
||||
return
|
||||
try:
|
||||
target_center = (
|
||||
float(self.edge_center_x_input.text()),
|
||||
float(self.edge_center_y_input.text()),
|
||||
float(self.edge_center_z_input.text()),
|
||||
)
|
||||
except (AttributeError, ValueError):
|
||||
QMessageBox.critical(self, "圆心坐标无效", "请输入 X, Y, Z 三个数字形式的目标圆心坐标。")
|
||||
return
|
||||
|
||||
edge_id = self.selected_edge_id
|
||||
plan = self.model.circular_edge_axis_move_plan(edge_id, target_center)
|
||||
lines = [
|
||||
f"Edge: {edge_id}",
|
||||
f"当前圆心: {_format_value(plan.get('current_edge_center'))}",
|
||||
f"目标圆心: {_format_value(plan.get('target_edge_center'))}",
|
||||
f"圆心移动: {_format_value(plan.get('circular_edge_center_move_vector'))}",
|
||||
f"相邻Face: {_format_value(plan.get('circular_edge_cylinder_face_id'))}",
|
||||
f"执行路径: {_format_value(plan.get('circular_edge_cylinder_mode_label'))}",
|
||||
f"当前轴心: {_format_value(plan.get('current_axis_center'))}",
|
||||
f"目标轴心: {_format_value(plan.get('target_axis_center'))}",
|
||||
f"圆边半径: {_format_value(plan.get('circular_edge_current_radius'))}",
|
||||
"当前版本会按圆Edge圆心的移动量,移动相邻孔/槽/凸台的圆柱轴心。",
|
||||
]
|
||||
if not self._confirm_quick_edit_plan(
|
||||
"圆Edge圆心/轴心移动",
|
||||
plan,
|
||||
lines,
|
||||
blocked_status="圆Edge圆心/轴心移动已阻止",
|
||||
cancelled_status="已取消圆Edge圆心/轴心移动",
|
||||
):
|
||||
return
|
||||
|
||||
self.clear_edit_preview(render=False)
|
||||
|
||||
def action():
|
||||
return self.model.move_circular_edge_axis_center(edge_id, target_center)
|
||||
|
||||
self._run_edit_action(
|
||||
action,
|
||||
operation_name="移动圆Edge相邻轴心",
|
||||
target=f"Edge {edge_id}",
|
||||
parameters={
|
||||
"part_id": plan.get("part_id"),
|
||||
"solid_id": plan.get("solid_id"),
|
||||
"curve": plan.get("curve"),
|
||||
"current_edge_center": plan.get("current_edge_center"),
|
||||
"target_edge_center": plan.get("target_edge_center"),
|
||||
"circular_edge_center_move_vector": plan.get("circular_edge_center_move_vector"),
|
||||
"circular_edge_center_move_distance": plan.get("circular_edge_center_move_distance"),
|
||||
"circular_edge_current_radius": plan.get("circular_edge_current_radius"),
|
||||
"circular_edge_current_diameter": plan.get("circular_edge_current_diameter"),
|
||||
"circular_edge_cylinder_face_id": plan.get("circular_edge_cylinder_face_id"),
|
||||
"circular_edge_cylinder_mode": plan.get("circular_edge_cylinder_mode"),
|
||||
"circular_edge_cylinder_mode_label": plan.get("circular_edge_cylinder_mode_label"),
|
||||
"current_axis_center": plan.get("current_axis_center"),
|
||||
"target_axis_center": plan.get("target_axis_center"),
|
||||
"axis_move_vector": plan.get("axis_move_vector"),
|
||||
"axis_move_distance": plan.get("axis_move_distance"),
|
||||
"axis_move_radial_distance": plan.get("axis_move_radial_distance"),
|
||||
"axis_move_axial_delta": plan.get("axis_move_axial_delta"),
|
||||
"target_diameter": plan.get("target_diameter"),
|
||||
"resize_strategy": plan.get("resize_strategy"),
|
||||
"edit_strategy_label": plan.get("edit_strategy_label"),
|
||||
"edit_semantics": plan.get("edit_semantics"),
|
||||
"move_axis_status": plan.get("status"),
|
||||
"move_axis_risk": plan.get("risk"),
|
||||
"move_axis_message": plan.get("message"),
|
||||
"move_axis_warnings": plan.get("warnings"),
|
||||
"move_axis_blockers": plan.get("blockers"),
|
||||
"ui_preview": "skipped-to-avoid-ui-freeze",
|
||||
},
|
||||
target_kind="edge",
|
||||
target_id=edge_id,
|
||||
)
|
||||
|
||||
def move_edge_center_point(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
@@ -3996,6 +4178,9 @@ class WindowActionMixin:
|
||||
"delta_reference_radius": plan.get("delta_reference_radius"),
|
||||
"reference_radius_delta_ratio": plan.get("reference_radius_delta_ratio"),
|
||||
"semi_angle": plan.get("semi_angle"),
|
||||
"semi_angle_degrees": plan.get("semi_angle_degrees"),
|
||||
"target_semi_angle": plan.get("target_semi_angle"),
|
||||
"target_semi_angle_degrees": plan.get("target_semi_angle_degrees"),
|
||||
"affine_scale": plan.get("affine_scale"),
|
||||
"affine_transform_kind": plan.get("affine_transform_kind"),
|
||||
"affine_transform_label": plan.get("affine_transform_label"),
|
||||
@@ -4245,7 +4430,12 @@ class WindowActionMixin:
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
"这不是只改变一个面的 CAD 历史面积参数;继续操作会缩放所属对象并刷新 B-Rep 结果几何,支持失败回滚/撤销。确定继续吗?"
|
||||
+ (
|
||||
"本次会在隔离子进程里执行高风险 OCC 计算;如果子进程卡死或崩溃,主程序和原模型会保持不变。\n\n"
|
||||
if str(plan.get("risk")) == "high"
|
||||
else ""
|
||||
)
|
||||
+ "这不是只改变一个面的 CAD 历史面积参数;继续操作会缩放所属对象并刷新 B-Rep 结果几何,支持失败回滚/撤销。确定继续吗?"
|
||||
),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
@@ -4288,6 +4478,7 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=self._isolation_for_plan(plan, "resize_face_area", [face_id, target_area]),
|
||||
)
|
||||
|
||||
def resize_face_area_local(self) -> None:
|
||||
@@ -4363,6 +4554,7 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=self._isolation_for_plan(plan, "resize_face_area_local", [face_id, target_area]),
|
||||
)
|
||||
|
||||
def resize_face_width_local(self) -> None:
|
||||
@@ -4466,6 +4658,7 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=self._isolation_for_plan(plan, "resize_face_size_local", [face_id, target_size, axis_key]),
|
||||
)
|
||||
|
||||
def _resize_face_size_owning_scale(self, axis: str) -> None:
|
||||
@@ -4569,6 +4762,7 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=self._isolation_for_plan(plan, "resize_face_size_owning_scale", [face_id, target_size, axis_key]),
|
||||
)
|
||||
|
||||
def move_selected_face_plane_position_by_translation(self) -> None:
|
||||
@@ -4585,42 +4779,15 @@ class WindowActionMixin:
|
||||
QMessageBox.critical(self, "面偏移无效", "请输入数字形式的目标面偏移。")
|
||||
return
|
||||
face_id = self.selected_face_id
|
||||
frame = self._selected_plane_offset_frame(face_id)
|
||||
if frame is None:
|
||||
QMessageBox.information(self, "不能按面偏移平移", "当前 Face 缺少稳定平面方向或基准点。")
|
||||
return
|
||||
plane_origin, plane_direction, current_position = frame
|
||||
target_position = current_position + distance
|
||||
vector = (
|
||||
plane_direction[0] * distance,
|
||||
plane_direction[1] * distance,
|
||||
plane_direction[2] * distance,
|
||||
)
|
||||
|
||||
if self.selected_solid_id is not None:
|
||||
moved_kind = "solid"
|
||||
moved_id = self.selected_solid_id
|
||||
plan = self.model.translate_solid_plan(moved_id, vector)
|
||||
|
||||
def action():
|
||||
return self.model.translate_solid(moved_id, vector)
|
||||
|
||||
elif self.selected_part_id is not None:
|
||||
moved_kind = "part"
|
||||
moved_id = self.selected_part_id
|
||||
plan = self.model.translate_part_plan(moved_id, vector)
|
||||
|
||||
def action():
|
||||
return self.model.translate_part(moved_id, vector)
|
||||
|
||||
else:
|
||||
QMessageBox.information(self, "不能按面偏移平移", "当前 Face 没有关联到稳定的所属对象。")
|
||||
return
|
||||
plan = self.model.face_plane_offset_owning_translation_plan(face_id, distance)
|
||||
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能按面偏移平移", str(plan["message"]))
|
||||
self.statusBar().showMessage("面偏移整体平移已阻止")
|
||||
return
|
||||
current_position = float(plan.get("current_plane_position", 0.0) or 0.0)
|
||||
target_position = float(plan.get("target_plane_position", current_position) or current_position)
|
||||
plane_direction = tuple(plan.get("plane_direction") or (0.0, 0.0, 0.0))
|
||||
if not self._confirm_face_plane_position_translation_plan(
|
||||
plan,
|
||||
face_id,
|
||||
@@ -4631,6 +4798,12 @@ class WindowActionMixin:
|
||||
self.statusBar().showMessage("已取消面偏移整体平移")
|
||||
return
|
||||
self.clear_edit_preview(render=False)
|
||||
|
||||
def action():
|
||||
return self.model.translate_face_plane_offset_owning(face_id, distance)
|
||||
|
||||
moved_kind = str(plan.get("target_kind", ""))
|
||||
moved_id = plan.get("solid_id") if moved_kind == "solid" else plan.get("part_id")
|
||||
self._run_edit_action(
|
||||
action,
|
||||
operation_name="面偏移(整体)",
|
||||
@@ -4643,9 +4816,9 @@ class WindowActionMixin:
|
||||
"moved_target_id": moved_id,
|
||||
"current_plane_position": current_position,
|
||||
"target_plane_position": target_position,
|
||||
"plane_origin": plane_origin,
|
||||
"plane_origin": plan.get("plane_origin"),
|
||||
"plane_direction": plane_direction,
|
||||
"translation_vector": vector,
|
||||
"translation_vector": plan.get("translation_vector"),
|
||||
"translation_distance": plan.get("translation_distance"),
|
||||
"bbox_diagonal": plan.get("bbox_diagonal"),
|
||||
"translate_status": plan.get("status"),
|
||||
@@ -4655,7 +4828,7 @@ class WindowActionMixin:
|
||||
"translate_blockers": plan.get("blockers"),
|
||||
"resize_strategy": "translate-owning-shape-from-plane-offset",
|
||||
"edit_strategy_label": "按面偏移平移所属对象",
|
||||
"edit_semantics": "把目标面偏移换算成沿当前平面方向的平移量,并平移所属特征或 Solid;不推拉当前面,不切削,也不补料。",
|
||||
"edit_semantics": "把目标面偏移换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid;不推拉当前面,不切削,也不补料。",
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
@@ -4676,33 +4849,12 @@ class WindowActionMixin:
|
||||
return
|
||||
|
||||
face_id = self.selected_face_id
|
||||
frame = self._selected_plane_offset_frame(face_id)
|
||||
if frame is None:
|
||||
QMessageBox.information(self, "不能只移动当前Face", "当前 Face 缺少稳定平面方向或基准点。")
|
||||
return
|
||||
plane_origin, plane_direction, current_position = frame
|
||||
current_center = self._selected_face_center_for_translation(face_id)
|
||||
if current_center is None:
|
||||
QMessageBox.information(self, "不能只移动当前Face", "当前 Face 缺少稳定中心坐标。")
|
||||
return
|
||||
|
||||
target_position = current_position + distance
|
||||
vector = (
|
||||
plane_direction[0] * distance,
|
||||
plane_direction[1] * distance,
|
||||
plane_direction[2] * distance,
|
||||
)
|
||||
target_center = (
|
||||
current_center[0] + vector[0],
|
||||
current_center[1] + vector[1],
|
||||
current_center[2] + vector[2],
|
||||
)
|
||||
plan = self.model.face_center_local_move_plan(face_id, target_center)
|
||||
plan = self.model.face_plane_offset_local_plan(face_id, distance)
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
f"当前面偏移: {_format_value(current_position)}",
|
||||
f"目标面偏移: {_format_value(target_position)}",
|
||||
f"平面方向: {_format_value(plane_direction)}",
|
||||
f"当前面偏移: {_format_value(plan.get('current_plane_position'))}",
|
||||
f"目标面偏移: {_format_value(plan.get('target_plane_position'))}",
|
||||
f"平面方向: {_format_value(plan.get('plane_direction'))}",
|
||||
f"移动向量: {_format_value(plan.get('face_center_move_vector'))}",
|
||||
f"移动距离: {_format_value(plan.get('face_center_move_distance'))}",
|
||||
f"移动/所属对象尺寸: {_format_percent(plan.get('face_center_move_ratio'))}",
|
||||
@@ -4721,7 +4873,7 @@ class WindowActionMixin:
|
||||
self.clear_edit_preview(render=False)
|
||||
|
||||
def action():
|
||||
return self.model.move_face_center_local(face_id, target_center)
|
||||
return self.model.move_face_plane_offset_local(face_id, distance)
|
||||
|
||||
self._run_edit_action(
|
||||
action,
|
||||
@@ -4732,10 +4884,10 @@ class WindowActionMixin:
|
||||
"part_id": plan.get("part_id"),
|
||||
"solid_id": plan.get("solid_id"),
|
||||
"surface": plan.get("surface"),
|
||||
"current_plane_position": current_position,
|
||||
"target_plane_position": target_position,
|
||||
"plane_origin": plane_origin,
|
||||
"plane_direction": plane_direction,
|
||||
"current_plane_position": plan.get("current_plane_position"),
|
||||
"target_plane_position": plan.get("target_plane_position"),
|
||||
"plane_origin": plan.get("plane_origin"),
|
||||
"plane_direction": plan.get("plane_direction"),
|
||||
"plane_offset_distance": distance,
|
||||
"current_face_center": plan.get("current_face_center"),
|
||||
"target_face_center": plan.get("target_face_center"),
|
||||
@@ -4749,7 +4901,7 @@ class WindowActionMixin:
|
||||
"resize_strategy": "local-face-plane-offset-deform",
|
||||
"edit_strategy_label": "按面偏移只移动当前Face",
|
||||
"edit_semantics": (
|
||||
"把目标面偏移换算成沿当前平面方向的移动量,只移动当前 Face 的顶点并重建相邻平面;"
|
||||
"把目标面偏移换算成沿当前面垂直方向的移动量,只移动当前 Face 的顶点并重建相邻平面;"
|
||||
"不推拉加料/切削,也不平移所属对象。"
|
||||
),
|
||||
"resize_status": plan.get("status"),
|
||||
@@ -4761,6 +4913,7 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=self._isolation_for_plan(plan, "move_face_plane_offset_local", [face_id, distance]),
|
||||
)
|
||||
|
||||
def move_selected_face_center(self) -> None:
|
||||
@@ -4787,18 +4940,18 @@ class WindowActionMixin:
|
||||
if self.selected_solid_id is not None:
|
||||
moved_kind = "solid"
|
||||
moved_id = self.selected_solid_id
|
||||
plan = self.model.translate_solid_plan(moved_id, vector)
|
||||
plan = self.model.face_center_owning_translation_plan(face_id, target_center)
|
||||
|
||||
def action():
|
||||
return self.model.translate_solid(moved_id, vector)
|
||||
return self.model.move_face_center_owning(face_id, target_center)
|
||||
|
||||
elif self.selected_part_id is not None:
|
||||
moved_kind = "part"
|
||||
moved_id = self.selected_part_id
|
||||
plan = self.model.translate_part_plan(moved_id, vector)
|
||||
plan = self.model.face_center_owning_translation_plan(face_id, target_center)
|
||||
|
||||
def action():
|
||||
return self.model.translate_part(moved_id, vector)
|
||||
return self.model.move_face_center_owning(face_id, target_center)
|
||||
|
||||
else:
|
||||
QMessageBox.information(self, "不能移动Face中心", "当前 Face 没有关联到稳定的所属对象。")
|
||||
@@ -4918,6 +5071,7 @@ class WindowActionMixin:
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
isolation=self._isolation_for_plan(plan, "move_face_center_local", [face_id, list(target_center)]),
|
||||
)
|
||||
|
||||
def move_selected_axis_center_by_translation(self) -> None:
|
||||
@@ -5181,11 +5335,11 @@ class WindowActionMixin:
|
||||
f"平移向量: {_format_value(plan['translation_vector'])}\n"
|
||||
f"平移距离: {_format_value(plan['translation_distance'])}\n"
|
||||
f"编辑策略: 按面偏移平移所属对象\n"
|
||||
f"编辑方式: 沿当前平面方向平移所属特征或 Solid;不推拉当前面,不切削,也不补料。\n"
|
||||
f"编辑方式: 沿当前面垂直方向平移所属特征或 Solid;不推拉当前面,不切削,也不补料。\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
"如果你想改变厚度或把这个面推出/切入,请使用不带“整体”的面偏移或偏移距离行。确定继续吗?"
|
||||
"如果你想改变厚度或把这个面推出/切入,请在“面偏移”这一行把影响范围选为“推拉当前面”。确定继续吗?"
|
||||
),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
@@ -6023,7 +6177,7 @@ class WindowActionMixin:
|
||||
"该操作被稳定性保护阻止。\n\n"
|
||||
"当前计划被判定为 high risk;这类 OCCT 布尔、倒圆或局部重建在复杂 STEP 上"
|
||||
"可能不是普通失败,而是让进程卡死或直接退出。请先尝试更小的参数、修复模型、"
|
||||
"选择更明确的面/边,或等后续子进程隔离执行通道实现后再开放。"
|
||||
"选择更明确的面/边;当前这类操作还没有接入隔离子进程执行通道。"
|
||||
),
|
||||
)
|
||||
self.statusBar().showMessage("高风险操作已被稳定性保护阻止")
|
||||
@@ -6037,6 +6191,7 @@ class WindowActionMixin:
|
||||
parameters: dict[str, object],
|
||||
target_kind: str | None = None,
|
||||
target_id: int | None = None,
|
||||
isolation: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
@@ -6057,6 +6212,7 @@ class WindowActionMixin:
|
||||
"pick_position": self.selected_pick_position,
|
||||
"show_same_domain_internal_edges": self._show_same_domain_internal_edges(),
|
||||
"edit_result_deflection": result_deflection,
|
||||
"isolation": dict(isolation or {}),
|
||||
}
|
||||
|
||||
self._begin_edit_task(operation_name)
|
||||
@@ -6097,6 +6253,16 @@ class WindowActionMixin:
|
||||
before_stats = self.model.stats()
|
||||
before_part_stats = self._part_stats_or_none(target_part_id)
|
||||
before_geometry = {}
|
||||
isolation = context.get("isolation")
|
||||
if isinstance(isolation, dict) and isolation:
|
||||
return self._run_isolated_edit_job(
|
||||
context=context,
|
||||
isolation=isolation,
|
||||
snapshot=snapshot,
|
||||
before_stats=before_stats,
|
||||
before_part_stats=before_part_stats,
|
||||
before_geometry=before_geometry,
|
||||
)
|
||||
try:
|
||||
result = action()
|
||||
after_snapshot = self.model.snapshot()
|
||||
@@ -6140,6 +6306,120 @@ class WindowActionMixin:
|
||||
|
||||
return job
|
||||
|
||||
def _run_isolated_edit_job(
|
||||
self,
|
||||
*,
|
||||
context: dict[str, object],
|
||||
isolation: dict[str, object],
|
||||
snapshot: dict[object, object],
|
||||
before_stats,
|
||||
before_part_stats,
|
||||
before_geometry: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
if self.model is None:
|
||||
raise RuntimeError("Model is not loaded.")
|
||||
target_part_id = self._edit_context_part_id(context)
|
||||
timeout_seconds = float(isolation.get("timeout_seconds") or 180.0)
|
||||
operation = str(isolation.get("operation") or "").strip()
|
||||
args = list(isolation.get("args") or [])
|
||||
if not operation:
|
||||
raise RuntimeError("隔离执行缺少操作名称。")
|
||||
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
with tempfile.TemporaryDirectory(prefix="geom_param_isolated_edit_") as temp_dir:
|
||||
temp_root = Path(temp_dir)
|
||||
input_path = temp_root / "input.step"
|
||||
output_path = temp_root / "output.step"
|
||||
request_path = temp_root / "request.json"
|
||||
self.model.export_all(input_path)
|
||||
request_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"input_path": str(input_path),
|
||||
"output_path": str(output_path),
|
||||
"operation": operation,
|
||||
"args": args,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
command = [sys.executable, "-m", "step_editor.isolated_edit_worker", str(request_path)]
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
cwd=project_root,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=timeout_seconds,
|
||||
check=False,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise RuntimeError(
|
||||
f"隔离子进程执行超时,已终止危险计算;主程序和原模型保持不变。"
|
||||
f" 超时时间: {timeout_seconds:g}s"
|
||||
) from exc
|
||||
|
||||
response_path = request_path.with_suffix(".response.json")
|
||||
response: dict[str, object] = {}
|
||||
if response_path.exists():
|
||||
try:
|
||||
response = json.loads(response_path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
response = {}
|
||||
if completed.returncode != 0 or not bool(response.get("ok", False)):
|
||||
error = str(response.get("error") or completed.stderr or completed.stdout or "未知错误").strip()
|
||||
raise RuntimeError(
|
||||
"隔离子进程执行失败;主程序没有崩溃,原模型保持不变。"
|
||||
f" 子进程返回码: {completed.returncode}. 错误: {error}"
|
||||
)
|
||||
if not output_path.exists():
|
||||
raise RuntimeError("隔离子进程报告成功,但没有生成结果 STEP;原模型保持不变。")
|
||||
|
||||
new_model = StepModel.load(output_path)
|
||||
try:
|
||||
new_model.filename = self.step_path
|
||||
except Exception:
|
||||
pass
|
||||
self.model = new_model
|
||||
|
||||
after_snapshot = self.model.snapshot()
|
||||
after_stats = self.model.stats()
|
||||
after_part_stats = self._part_stats_or_none(target_part_id)
|
||||
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(
|
||||
deflection=deflection,
|
||||
show_same_domain_internal_edges=bool(context.get("show_same_domain_internal_edges", False)),
|
||||
)
|
||||
except Exception:
|
||||
model_polydata = None
|
||||
edge_polydata = None
|
||||
|
||||
child_message = str(response.get("message") or "隔离子进程编辑完成。")
|
||||
return {
|
||||
"message": f"{child_message} 已通过隔离子进程完成;如果 OCC 崩溃,主程序不会被带崩。",
|
||||
"snapshot": snapshot,
|
||||
"before_stats": before_stats,
|
||||
"before_part_stats": before_part_stats,
|
||||
"before_geometry": before_geometry,
|
||||
"after_snapshot": after_snapshot,
|
||||
"after_stats": after_stats,
|
||||
"after_part_stats": after_part_stats,
|
||||
"quality_warnings": _edit_quality_warnings(before_part_stats, after_part_stats),
|
||||
"after_geometry": after_geometry,
|
||||
"model_polydata": model_polydata,
|
||||
"edge_polydata": edge_polydata,
|
||||
}
|
||||
|
||||
def _edit_context_part_id(self, context: dict[str, object]) -> int | None:
|
||||
if self.model is None:
|
||||
return None
|
||||
@@ -6218,9 +6498,9 @@ class WindowActionMixin:
|
||||
elif "面宽(当前面)" in operation_name or "面高(当前面)" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会只沿选中 Face 的一个平面内方向缩放顶点并重建相邻平面。"
|
||||
elif "面偏移(当前面)" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会沿平面方向移动所选 Face 顶点并重建相邻平面。"
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会沿当前面垂直方向移动所选 Face 顶点并重建相邻平面。"
|
||||
elif "面偏移(整体)" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会沿平面方向平移所属特征或 Solid,不推拉当前面。"
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会沿当前面垂直方向平移所属特征或 Solid,不推拉当前面。"
|
||||
elif "Face中心" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会平移所属特征或 Solid,不做单面局部扭曲。"
|
||||
elif "只移动当前Face" in operation_name:
|
||||
|
||||
+227
-62
@@ -1313,6 +1313,9 @@ class WindowStateMixin:
|
||||
_int_values(action_info.get("feature_start_end_face_ids"))
|
||||
or _int_values(action_info.get("feature_end_end_face_ids"))
|
||||
)
|
||||
show_generic_face_edit_specs = bool(has_face and (is_plane or is_shell_candidate))
|
||||
local_face_deform_ready = bool(action_info.get("local_face_deform_ready", True))
|
||||
local_face_deform_blocker = str(action_info.get("local_face_deform_blocker") or "").strip()
|
||||
has_fillet_support = len(_int_values(action_info.get("feature_existing_fillet_support_face_ids"))) >= 2
|
||||
is_line_edge = has_edge and curve == "line"
|
||||
specs: list[dict[str, object]] = []
|
||||
@@ -1327,28 +1330,65 @@ class WindowStateMixin:
|
||||
return ""
|
||||
return ", ".join(_format_float(item) for item in value)
|
||||
|
||||
def relative_range_hint(current: object, caution_ratio: float, high_ratio: float) -> str:
|
||||
def relative_range_hint(
|
||||
current: object,
|
||||
caution_ratio: float,
|
||||
high_ratio: float,
|
||||
hard_limits: str = "",
|
||||
) -> str:
|
||||
number = _float_or_none(current)
|
||||
if number is None or number <= 0:
|
||||
return "建议先小幅试改并查看预览,过大变化可能导致布尔失败或周边变形。"
|
||||
base = "建议先小幅试改并查看预览,过大变化可能导致布尔失败或周边变形。"
|
||||
return f"{base} {hard_limits}".strip()
|
||||
lower = max(number * (1.0 - caution_ratio), 0.0)
|
||||
upper = number * (1.0 + caution_ratio)
|
||||
return (
|
||||
base = (
|
||||
f"建议先在 {_format_float(lower)} - {_format_float(upper)} 内试改"
|
||||
f"(相对当前值约 +/-{caution_ratio * 100.0:.0f}%);"
|
||||
f"变化超过 {high_ratio * 100.0:.0f}% 时风险较高。"
|
||||
)
|
||||
return f"{base} {hard_limits}".strip()
|
||||
|
||||
def positive_minimum() -> dict[str, object]:
|
||||
return {"min_value": 0.0, "min_exclusive": True}
|
||||
|
||||
def face_scale_limits(current: object, *, squared: bool = False) -> dict[str, object]:
|
||||
number = _float_or_none(current)
|
||||
if number is None or number <= 0:
|
||||
return positive_minimum()
|
||||
power = 2 if squared else 1
|
||||
return {
|
||||
"min_value": number * (0.05 ** power),
|
||||
"max_value": number * (5.0 ** power),
|
||||
}
|
||||
|
||||
def face_offset_target_limits(current_position: object) -> dict[str, object]:
|
||||
position = _float_or_none(current_position)
|
||||
diagonal = _float_or_none(action_info.get("bbox_diagonal"))
|
||||
if position is None or diagonal is None or diagonal <= 0:
|
||||
return {}
|
||||
span = diagonal * 5.0
|
||||
return {"min_value": position - span, "max_value": position + span}
|
||||
|
||||
def face_vector_move_limit(reference: object) -> dict[str, object]:
|
||||
center = _triple_or_none(reference)
|
||||
diagonal = _float_or_none(action_info.get("bbox_diagonal"))
|
||||
if center is None or diagonal is None or diagonal <= 0:
|
||||
return {}
|
||||
return {
|
||||
"vector_distance_reference": center,
|
||||
"max_vector_distance": diagonal * 5.0,
|
||||
"vector_distance_label": "中心移动距离",
|
||||
}
|
||||
|
||||
def push_pull_hint() -> str:
|
||||
diagonal = _float_or_none(action_info.get("bbox_diagonal"))
|
||||
if diagonal is not None and diagonal > 0:
|
||||
return (
|
||||
"可输入正数或负数,单位同模型;"
|
||||
f"建议单次绝对值不超过 {_format_float(diagonal * 0.08)},"
|
||||
f"超过 {_format_float(diagonal * 0.2)} 时风险较高。"
|
||||
f"超过 {_format_float(diagonal * 0.2)} 时风险较高;"
|
||||
f"超过 {_format_float(diagonal * 5.0)} 会被阻止。"
|
||||
)
|
||||
return "可输入正数或负数,单位同模型;建议先小幅试改并查看预览。"
|
||||
|
||||
@@ -1362,6 +1402,11 @@ class WindowStateMixin:
|
||||
)
|
||||
return "格式为 X, Y, Z;建议先小幅试改并查看预览。"
|
||||
|
||||
face_linear_hard_limit = "目标值低于当前值的 5% 或高于当前值的 5 倍会被阻止。"
|
||||
face_area_hard_limit = "如果目标面积会让面宽/面高缩到当前 5% 以下或放大到 5 倍以上,会被阻止。"
|
||||
face_offset_hard_limit = "如果目标位置与当前位置差距远超当前模型尺寸,会被阻止。"
|
||||
face_center_hard_limit = "如果目标中心与当前中心距离超过所属对象尺寸的 5 倍,会被阻止。"
|
||||
|
||||
def hole_diameter_upper_limit(current: object) -> float | None:
|
||||
current_number = _float_or_none(current)
|
||||
height = _float_or_none(action_info.get("height_estimate"))
|
||||
@@ -1406,6 +1451,11 @@ class WindowStateMixin:
|
||||
"高度、厚度和其它尺寸会同比例变化;如果只想改孔壁/槽壁,请使用普通半径行。"
|
||||
)
|
||||
|
||||
def face_local_disabled_tip(base: str) -> str:
|
||||
if local_face_deform_ready or not local_face_deform_blocker:
|
||||
return base
|
||||
return f"{base} 当前不能只改当前面的原因:{local_face_deform_blocker}"
|
||||
|
||||
def add_spec(
|
||||
*,
|
||||
key: str,
|
||||
@@ -1591,14 +1641,24 @@ class WindowStateMixin:
|
||||
),
|
||||
)
|
||||
elif is_plane or is_shell_candidate:
|
||||
face_semantics_text = "Face:先改目标值,再用“影响范围”选择改当前面、推拉或调整整个特征。"
|
||||
face_semantics_tip = (
|
||||
"面积是面的大小;面宽/面高是这个面自身平面里的两个方向尺寸;"
|
||||
"面偏移是沿当前面垂直方向测到的位置。影响范围决定这次修改只作用在当前 Face,"
|
||||
"还是推拉加料/切削,或带动所属特征或 Solid。"
|
||||
)
|
||||
if is_plane and not local_face_deform_ready:
|
||||
face_semantics_text = "Face:当前面暂不能只改当前面,可用推拉或整体策略。"
|
||||
face_semantics_tip = (
|
||||
"此 Face 不适合做“只改当前面”的局部顶点重建;"
|
||||
f"原因:{local_face_deform_blocker or '当前拓扑不满足局部重建条件。'}"
|
||||
"可优先使用“推拉当前面”、孔/槽专门入口,或选择移动/调整整个特征。"
|
||||
)
|
||||
add_readonly_spec(
|
||||
key="face_edit_semantics",
|
||||
label="编辑方式",
|
||||
text="Face:先改目标值,再用“影响范围”选择只改当前面或调整整个特征。",
|
||||
tip=(
|
||||
"面积、面宽、面高、中心和面偏移会尽量合并成一行参数;"
|
||||
"影响范围下拉框决定这次修改是只作用在当前 Face,还是带动所属特征或 Solid。"
|
||||
),
|
||||
text=face_semantics_text,
|
||||
tip=face_semantics_tip,
|
||||
)
|
||||
|
||||
if has_edge:
|
||||
@@ -1612,7 +1672,7 @@ class WindowStateMixin:
|
||||
),
|
||||
)
|
||||
|
||||
if has_face:
|
||||
if show_generic_face_edit_specs:
|
||||
current_face_area = _float_or_none(action_info.get("area"))
|
||||
current_face_center = (
|
||||
_triple_or_none(action_info.get("area_center"))
|
||||
@@ -1631,6 +1691,7 @@ class WindowStateMixin:
|
||||
"target_attr": "face_area_input",
|
||||
"enabled": bool(
|
||||
is_plane
|
||||
and local_face_deform_ready
|
||||
and current_face_area is not None
|
||||
and current_face_area > 0
|
||||
and current_face_center is not None
|
||||
@@ -1639,10 +1700,12 @@ class WindowStateMixin:
|
||||
"输入目标面积;程序只在当前 Face 平面内缩放这个面的顶点,"
|
||||
"相邻面会按新边界重建。"
|
||||
),
|
||||
"disabled_tip": "只有带稳定面积和中心坐标的平面 Face 才能尝试只修改当前面面积。",
|
||||
"disabled_tip": face_local_disabled_tip(
|
||||
"只有带稳定面积和中心坐标的平面 Face 才能尝试只修改当前面面积。"
|
||||
),
|
||||
"range_hint": (
|
||||
"只改当前面时,相邻面可能自然变斜。当前只对简单全平面实体开放,"
|
||||
f"建议先小幅修改。 {relative_range_hint(current_face_area, 0.15, 0.8)}"
|
||||
f"建议先小幅修改。 {relative_range_hint(current_face_area, 0.15, 0.8, face_area_hard_limit)}"
|
||||
),
|
||||
},
|
||||
"owning": {
|
||||
@@ -1657,24 +1720,24 @@ class WindowStateMixin:
|
||||
"disabled_tip": "当前 Face 缺少稳定面积,不能按目标面积缩放所属对象。",
|
||||
"range_hint": (
|
||||
"调整整个特征会影响同一对象上的其它尺寸。"
|
||||
f" {relative_range_hint(current_face_area, 0.15, 0.8)}"
|
||||
f" {relative_range_hint(current_face_area, 0.15, 0.8, face_area_hard_limit)}"
|
||||
),
|
||||
},
|
||||
},
|
||||
value_type="positive",
|
||||
used=("area", "area_center", "bbox_center"),
|
||||
**positive_minimum(),
|
||||
**face_scale_limits(current_face_area, squared=True),
|
||||
)
|
||||
if is_plane:
|
||||
current_face_width = _float_or_none(action_info.get("local_face_width"))
|
||||
current_face_height = _float_or_none(action_info.get("local_face_height"))
|
||||
face_size_tip = (
|
||||
"这里的宽/高是选中 Face 在自身平面内两个稳定方向上的投影尺寸,"
|
||||
"不是全局 X/Y/Z 包围盒尺寸。只修改当前 Face 顶点,相邻面会按新顶点重建。"
|
||||
"这里的面宽/面高是选中 Face 在自身平面内两个稳定方向上的投影长度,"
|
||||
"不是面积,也不是模型整体高度。只修改当前 Face 顶点,相邻面会按新顶点重建。"
|
||||
)
|
||||
face_size_owner_tip = (
|
||||
"这里的宽/高是选中 Face 在自身平面内两个稳定方向上的投影尺寸,"
|
||||
"不是全局 X/Y/Z 包围盒尺寸。程序会沿该方向缩放所属特征或 Solid,其它几何会跟随变化。"
|
||||
"这里的面宽/面高是选中 Face 在自身平面内两个稳定方向上的投影长度,"
|
||||
"不是面积,也不是模型整体高度。程序会沿该方向缩放所属特征或 Solid,其它几何会跟随变化。"
|
||||
)
|
||||
add_scoped_spec(
|
||||
key="local_face_width",
|
||||
@@ -1688,13 +1751,16 @@ class WindowStateMixin:
|
||||
"action": "resize_face_width_local",
|
||||
"target_attr": "face_width_input",
|
||||
"enabled": bool(
|
||||
current_face_width is not None
|
||||
local_face_deform_ready
|
||||
and current_face_width is not None
|
||||
and current_face_width > 0
|
||||
and current_face_center is not None
|
||||
),
|
||||
"enabled_tip": f"输入目标面宽;{face_size_tip}",
|
||||
"disabled_tip": "只有带稳定顶点环和中心坐标的平面 Face 才能尝试修改当前面宽。",
|
||||
"range_hint": f"{face_size_tip} {relative_range_hint(current_face_width, 0.25, 0.8)}",
|
||||
"disabled_tip": face_local_disabled_tip(
|
||||
"只有带稳定顶点环和中心坐标的平面 Face 才能尝试修改当前面宽。"
|
||||
),
|
||||
"range_hint": f"{face_size_tip} {relative_range_hint(current_face_width, 0.25, 0.8, face_linear_hard_limit)}",
|
||||
},
|
||||
"owning": {
|
||||
"label": "调整整个特征",
|
||||
@@ -1707,13 +1773,13 @@ class WindowStateMixin:
|
||||
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
||||
),
|
||||
"enabled_tip": f"输入目标面宽;{face_size_owner_tip}",
|
||||
"disabled_tip": "当前 Face 缺少稳定面宽、中心坐标或所属对象,不能按面宽缩放所属对象。",
|
||||
"range_hint": f"{face_size_owner_tip} {relative_range_hint(current_face_width, 0.2, 0.5)}",
|
||||
"disabled_tip": "当前 Face 缺少稳定面宽、中心坐标或所属对象,不能按这个方向缩放所属对象。",
|
||||
"range_hint": f"{face_size_owner_tip} {relative_range_hint(current_face_width, 0.2, 0.5, face_linear_hard_limit)}",
|
||||
},
|
||||
},
|
||||
value_type="positive",
|
||||
used=("local_face_width", "local_face_width_direction", "local_face_size_center"),
|
||||
**positive_minimum(),
|
||||
**face_scale_limits(current_face_width),
|
||||
)
|
||||
add_scoped_spec(
|
||||
key="local_face_height",
|
||||
@@ -1727,13 +1793,16 @@ class WindowStateMixin:
|
||||
"action": "resize_face_height_local",
|
||||
"target_attr": "face_height_input",
|
||||
"enabled": bool(
|
||||
current_face_height is not None
|
||||
local_face_deform_ready
|
||||
and current_face_height is not None
|
||||
and current_face_height > 0
|
||||
and current_face_center is not None
|
||||
),
|
||||
"enabled_tip": f"输入目标面高;{face_size_tip}",
|
||||
"disabled_tip": "只有带稳定顶点环和中心坐标的平面 Face 才能尝试修改当前面高。",
|
||||
"range_hint": f"{face_size_tip} {relative_range_hint(current_face_height, 0.25, 0.8)}",
|
||||
"disabled_tip": face_local_disabled_tip(
|
||||
"只有带稳定顶点环和中心坐标的平面 Face 才能尝试修改当前面高。"
|
||||
),
|
||||
"range_hint": f"{face_size_tip} {relative_range_hint(current_face_height, 0.25, 0.8, face_linear_hard_limit)}",
|
||||
},
|
||||
"owning": {
|
||||
"label": "调整整个特征",
|
||||
@@ -1746,13 +1815,13 @@ class WindowStateMixin:
|
||||
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
||||
),
|
||||
"enabled_tip": f"输入目标面高;{face_size_owner_tip}",
|
||||
"disabled_tip": "当前 Face 缺少稳定面高、中心坐标或所属对象,不能按面高缩放所属对象。",
|
||||
"range_hint": f"{face_size_owner_tip} {relative_range_hint(current_face_height, 0.2, 0.5)}",
|
||||
"disabled_tip": "当前 Face 缺少稳定面高、中心坐标或所属对象,不能按这个方向缩放所属对象。",
|
||||
"range_hint": f"{face_size_owner_tip} {relative_range_hint(current_face_height, 0.2, 0.5, face_linear_hard_limit)}",
|
||||
},
|
||||
},
|
||||
value_type="positive",
|
||||
used=("local_face_height", "local_face_height_direction", "local_face_size_center"),
|
||||
**positive_minimum(),
|
||||
**face_scale_limits(current_face_height),
|
||||
)
|
||||
add_scoped_spec(
|
||||
key="face_center_position",
|
||||
@@ -1765,18 +1834,21 @@ class WindowStateMixin:
|
||||
"label": "只改当前面",
|
||||
"action": "move_selected_face_center_local",
|
||||
"target_attrs": ("translate_x_input", "translate_y_input", "translate_z_input"),
|
||||
"enabled": bool(is_plane and current_face_center is not None),
|
||||
"enabled": bool(is_plane and local_face_deform_ready and current_face_center is not None),
|
||||
"enabled_tip": (
|
||||
"输入这个 Face 中心要移动到的目标 X, Y, Z 坐标;"
|
||||
"程序只移动当前 Face 的顶点,并让相邻平面按新顶点重建。"
|
||||
),
|
||||
"disabled_tip": "只有带稳定中心坐标的平面 Face 才能尝试只移动当前 Face。",
|
||||
"disabled_tip": face_local_disabled_tip(
|
||||
"只有带稳定中心坐标的平面 Face 才能尝试只移动当前 Face。"
|
||||
),
|
||||
"range_hint": (
|
||||
"只改当前面时,相邻面可能自然变斜,非共面面可能被拆成三角面。"
|
||||
f"{translation_hint()}"
|
||||
f"{translation_hint()} {face_center_hard_limit}"
|
||||
),
|
||||
"target_transform": "target_center_to_translation",
|
||||
"transform_context": {"current_center": current_face_center},
|
||||
**face_vector_move_limit(current_face_center),
|
||||
},
|
||||
"owning": {
|
||||
"label": "移动整个特征",
|
||||
@@ -1791,10 +1863,11 @@ class WindowStateMixin:
|
||||
"disabled_tip": "当前 Face 缺少稳定中心坐标或所属对象,不能按中心坐标平移。",
|
||||
"range_hint": (
|
||||
"移动整个特征不会改变当前对象形状,但同一对象会整体搬动。"
|
||||
f"{translation_hint()}"
|
||||
f"{translation_hint()} {face_center_hard_limit}"
|
||||
),
|
||||
"target_transform": "target_center_to_translation",
|
||||
"transform_context": {"current_center": current_face_center},
|
||||
**face_vector_move_limit(current_face_center),
|
||||
},
|
||||
},
|
||||
value_type="vector3",
|
||||
@@ -1837,11 +1910,12 @@ class WindowStateMixin:
|
||||
"action": "push_pull_face",
|
||||
"target_attr": "offset_input",
|
||||
"enabled": current_plane_position is not None,
|
||||
"enabled_tip": "输入目标面偏移;程序会沿当前推拉方向移动面,自动加料或切削。",
|
||||
"enabled_tip": "输入目标面偏移;程序会沿当前面的垂直方向移动面,自动加料或切削。",
|
||||
"disabled_tip": "当前平面缺少稳定移动方向或基准点,不能按目标位置推拉。",
|
||||
"range_hint": (
|
||||
"这是沿当前推拉方向测量的位置值,不是 X/Y/Z 坐标;单位同模型。"
|
||||
"目标值大于当前值通常向外移,小于当前值通常向内移。"
|
||||
"面偏移是沿当前面垂直方向测量的目标值,不是面积、不是移动距离,也不是 X/Y/Z 坐标;单位同模型。"
|
||||
"程序会把目标面偏移自动换算成本次需要移动的距离。"
|
||||
f"{face_offset_hard_limit}"
|
||||
),
|
||||
"target_transform": "plane_target_position_to_offset",
|
||||
"transform_context": {"current_plane_position": current_plane_position},
|
||||
@@ -1852,18 +1926,21 @@ class WindowStateMixin:
|
||||
"target_attr": "offset_input",
|
||||
"enabled": bool(
|
||||
current_plane_position is not None
|
||||
and local_face_deform_ready
|
||||
and plane_direction is not None
|
||||
and current_face_center is not None
|
||||
),
|
||||
"enabled_tip": (
|
||||
"输入目标面偏移;程序只把当前 Face 沿平面方向移动到该位置,"
|
||||
"输入目标面偏移;程序只把当前 Face 沿当前面的垂直方向移动到该目标值,"
|
||||
"并让相邻平面按新顶点重建。"
|
||||
),
|
||||
"disabled_tip": "当前平面缺少稳定方向、基准点或中心,不能按面偏移只移动当前 Face。",
|
||||
"disabled_tip": face_local_disabled_tip(
|
||||
"当前平面缺少稳定方向、基准点或中心,不能按面偏移只移动当前 Face。"
|
||||
),
|
||||
"range_hint": (
|
||||
"只改当前面不会自动加料/切削,相邻面可能自然变斜,"
|
||||
"非共面面可能被拆成三角面。"
|
||||
f"{translation_hint()}"
|
||||
f"{translation_hint()} {face_offset_hard_limit}"
|
||||
),
|
||||
"target_transform": "plane_target_position_to_offset",
|
||||
"transform_context": {"current_plane_position": current_plane_position},
|
||||
@@ -1878,32 +1955,20 @@ class WindowStateMixin:
|
||||
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
||||
),
|
||||
"enabled_tip": (
|
||||
"输入目标面偏移;程序会沿当前平面方向平移所属特征或 Solid,"
|
||||
"输入目标面偏移;程序会沿当前面的垂直方向平移所属特征或 Solid,"
|
||||
"当前面形状和所属对象内部尺寸不变。"
|
||||
),
|
||||
"disabled_tip": "当前平面缺少稳定方向、基准点或所属对象,不能按面偏移整体平移。",
|
||||
"range_hint": (
|
||||
"这是整体移动所属对象,不是推拉当前面;如果想改变形状或厚度,请选择“推拉当前面”。"
|
||||
f"{translation_hint()}"
|
||||
f"{translation_hint()} {face_offset_hard_limit}"
|
||||
),
|
||||
"target_transform": "plane_target_position_to_offset",
|
||||
"transform_context": {"current_plane_position": current_plane_position},
|
||||
},
|
||||
},
|
||||
used=("plane_origin", "push_pull_outward_direction", "normal"),
|
||||
)
|
||||
add_spec(
|
||||
key="push_pull_distance",
|
||||
label="偏移距离",
|
||||
current_raw=0.0,
|
||||
target_text="0",
|
||||
action="push_pull_face",
|
||||
target_attr="offset_input",
|
||||
enabled=True,
|
||||
enabled_tip="输入本次要让这个面沿当前推拉方向移动多远;正负表示两个相反方向,执行后这里会重新从 0 开始。",
|
||||
disabled_tip="当前对象不是可移动的平面。",
|
||||
range_hint=push_pull_hint(),
|
||||
current_text="0(本次未移动)",
|
||||
**face_offset_target_limits(current_plane_position),
|
||||
)
|
||||
if is_shell_candidate:
|
||||
current = _float_or_none(action_info.get("shell_thickness_estimate"))
|
||||
@@ -1924,7 +1989,7 @@ class WindowStateMixin:
|
||||
"enabled": current is not None,
|
||||
"enabled_tip": "输入薄壁/壳体局部区域的目标厚度;程序会推拉当前平面来改变与相对面的距离。",
|
||||
"disabled_tip": "当前平面没有稳定识别到可修改的薄壁厚度。",
|
||||
"range_hint": relative_range_hint(current, 0.35, 0.8),
|
||||
"range_hint": relative_range_hint(current, 0.35, 0.8, face_linear_hard_limit),
|
||||
},
|
||||
"owning": {
|
||||
"label": "调整整个特征",
|
||||
@@ -1946,13 +2011,13 @@ class WindowStateMixin:
|
||||
"disabled_tip": "当前薄壁候选缺少稳定厚度、厚度方向、基准平面或所属对象,不能按厚度整体缩放。",
|
||||
"range_hint": (
|
||||
"这是整体缩放所属对象,不是推拉当前平面;"
|
||||
f"如果只想移动当前薄壁平面区域,请把影响范围设为“推拉当前面”。 {relative_range_hint(current, 0.2, 0.5)}"
|
||||
f"如果只想移动当前薄壁平面区域,请把影响范围设为“推拉当前面”。 {relative_range_hint(current, 0.2, 0.5, face_linear_hard_limit)}"
|
||||
),
|
||||
},
|
||||
},
|
||||
value_type="positive",
|
||||
used=("shell_thickness_estimate", "shell_current_thickness", "shell_signed_thickness", "normal", "plane_origin"),
|
||||
**positive_minimum(),
|
||||
**face_scale_limits(current),
|
||||
)
|
||||
if is_hole_or_groove:
|
||||
current_diameter = _float_or_none(action_info.get("diameter"))
|
||||
@@ -3180,7 +3245,28 @@ class WindowStateMixin:
|
||||
if is_circle_edge:
|
||||
current_radius = _float_or_none(action_info.get("radius"))
|
||||
current_diameter = _float_or_none(action_info.get("diameter"))
|
||||
circle_edge_center = _triple_or_none(action_info.get("center"))
|
||||
can_resize_circle = bool(current_length is not None and current_length > 0 and current_radius is not None and current_radius > 0)
|
||||
add_spec(
|
||||
key="circle_edge_axis_center",
|
||||
label="圆心/轴心",
|
||||
current_raw=circle_edge_center if circle_edge_center is not None else "",
|
||||
target_text=vector_text(circle_edge_center),
|
||||
action="move_circular_edge_axis_center",
|
||||
target_attrs=("edge_center_x_input", "edge_center_y_input", "edge_center_z_input"),
|
||||
enabled=bool(circle_edge_center is not None and current_radius is not None and current_radius > 0),
|
||||
enabled_tip=(
|
||||
"输入目标 X, Y, Z 坐标;程序会用圆Edge圆心的移动量,"
|
||||
"移动相邻孔、槽或凸台的圆柱轴心。"
|
||||
),
|
||||
disabled_tip="当前圆Edge缺少稳定圆心/半径,或没有可识别的相邻圆柱特征。",
|
||||
value_type="vector3",
|
||||
range_hint=(
|
||||
"格式为 X, Y, Z。这个修改只在点击“修改”时才识别相邻圆柱;"
|
||||
"优先移动局部孔/槽/凸台,不会整体平移零件。建议先小幅移动。"
|
||||
),
|
||||
used=("center", "adjacent_face_ids"),
|
||||
)
|
||||
circle_context = {
|
||||
"edge_current_length": current_length,
|
||||
"edge_current_radius": current_radius,
|
||||
@@ -3614,6 +3700,7 @@ class WindowStateMixin:
|
||||
target_widget.setToolTip(self._property_target_tooltip(effective_spec, editable=editable))
|
||||
widget = self.property_table.cellWidget(row, PROPERTY_ACTION_COLUMN)
|
||||
if isinstance(widget, QPushButton):
|
||||
validation_error = ""
|
||||
if str(effective_spec.get("value_type", "number")) == "command":
|
||||
row_changed = True
|
||||
widget.setText(str(effective_spec.get("button_text") or "执行"))
|
||||
@@ -3623,14 +3710,28 @@ class WindowStateMixin:
|
||||
text = self._property_target_text(row)
|
||||
row_changed = self._property_target_changed(effective_spec, text)
|
||||
empty_target = not bool(text.strip())
|
||||
widget.setText("未输入" if empty_target else ("应用" if row_changed else "未改动"))
|
||||
enabled = bool(has_model and effective_spec.get("enabled") and effective_spec.get("action") and row_changed)
|
||||
validation_error = "" if empty_target else self._property_target_validation_error(effective_spec, text)
|
||||
if empty_target:
|
||||
widget.setText("未输入")
|
||||
elif validation_error:
|
||||
widget.setText("无效")
|
||||
else:
|
||||
widget.setText("应用" if row_changed else "未改动")
|
||||
enabled = bool(
|
||||
has_model
|
||||
and effective_spec.get("enabled")
|
||||
and effective_spec.get("action")
|
||||
and row_changed
|
||||
and not validation_error
|
||||
)
|
||||
if empty_target:
|
||||
tooltip = (
|
||||
f"“{effective_spec.get('label', '当前属性')}”的目标值为空。"
|
||||
"这不会删除模型里的点、边或面,只是暂时没有要应用的目标值;"
|
||||
"重新选择对象会按当前模型值重新填入。"
|
||||
)
|
||||
elif validation_error:
|
||||
tooltip = f"{validation_error} 请调整目标值后再应用。"
|
||||
elif row_changed:
|
||||
scope_label = str(effective_spec.get("scope_label") or "").strip()
|
||||
suffix = f"({scope_label})" if scope_label else ""
|
||||
@@ -3640,7 +3741,8 @@ class WindowStateMixin:
|
||||
range_hint = self._property_range_hint(effective_spec)
|
||||
if range_hint:
|
||||
tooltip = f"{tooltip}\n\n{range_hint}"
|
||||
widget.setProperty("changed", bool(row_changed))
|
||||
widget.setProperty("changed", bool(row_changed and not validation_error))
|
||||
widget.setProperty("invalid", bool(validation_error))
|
||||
widget.setToolTip(tooltip)
|
||||
widget.setCursor(Qt.CursorShape.PointingHandCursor if enabled else Qt.CursorShape.ArrowCursor)
|
||||
widget.style().unpolish(widget)
|
||||
@@ -3679,7 +3781,7 @@ class WindowStateMixin:
|
||||
}:
|
||||
continue
|
||||
text = self._property_target_text(row)
|
||||
if self._property_target_changed(effective_spec, text):
|
||||
if self._property_target_changed(effective_spec, text) and not self._property_target_validation_error(effective_spec, text):
|
||||
changed.append((row, effective_spec, text))
|
||||
return changed
|
||||
|
||||
@@ -3728,6 +3830,65 @@ class WindowStateMixin:
|
||||
return abs(value - current) > PROPERTY_VALUE_TOLERANCE
|
||||
return None
|
||||
|
||||
def _property_target_validation_error(self, spec: dict[str, object], text: str) -> str:
|
||||
value_type = str(spec.get("value_type", "number"))
|
||||
if value_type == "command":
|
||||
return ""
|
||||
if not text.strip():
|
||||
return ""
|
||||
try:
|
||||
if value_type == "vector3":
|
||||
values = self._parse_property_vector3(text)
|
||||
return self._property_vector_distance_error(spec, values)
|
||||
if value_type == "choice":
|
||||
self._property_choice_value(spec, text)
|
||||
return ""
|
||||
if value_type == "number_pair":
|
||||
values = self._parse_property_number_pair(text)
|
||||
if spec.get("positive_pair") and any(value <= 0 for value in values):
|
||||
return "请输入两个大于 0 的目标值。"
|
||||
for value in values:
|
||||
range_error = self._property_scalar_range_error(spec, value)
|
||||
if range_error:
|
||||
return range_error
|
||||
return ""
|
||||
if value_type in {"integer", "integer_or_empty"}:
|
||||
if not text and value_type == "integer_or_empty":
|
||||
return ""
|
||||
try:
|
||||
value = int(text)
|
||||
except ValueError:
|
||||
return "请输入整数形式的目标值。"
|
||||
return self._property_scalar_range_error(spec, float(value))
|
||||
try:
|
||||
value = float(text)
|
||||
except ValueError:
|
||||
return "请输入数字形式的目标值。"
|
||||
if value_type == "positive" and value <= 0:
|
||||
return "请输入大于 0 的目标值。"
|
||||
return self._property_scalar_range_error(spec, value)
|
||||
except ValueError as exc:
|
||||
return str(exc)
|
||||
|
||||
def _property_vector_distance_error(
|
||||
self,
|
||||
spec: dict[str, object],
|
||||
values: tuple[float, float, float],
|
||||
) -> str:
|
||||
max_distance = _float_or_none(spec.get("max_vector_distance"))
|
||||
reference = _triple_or_none(spec.get("vector_distance_reference"))
|
||||
if max_distance is None or max_distance <= 0 or reference is None:
|
||||
return ""
|
||||
distance = math.sqrt(
|
||||
(values[0] - reference[0]) * (values[0] - reference[0])
|
||||
+ (values[1] - reference[1]) * (values[1] - reference[1])
|
||||
+ (values[2] - reference[2]) * (values[2] - reference[2])
|
||||
)
|
||||
if distance > max_distance + PROPERTY_VALUE_TOLERANCE:
|
||||
label = str(spec.get("vector_distance_label") or spec.get("label") or "目标距离")
|
||||
return f"{label} 必须小于或等于 {_format_float(max_distance)}。"
|
||||
return ""
|
||||
|
||||
def _property_target_changed(self, spec: dict[str, object], text: str) -> bool:
|
||||
value_type = str(spec.get("value_type", "number"))
|
||||
if not text:
|
||||
@@ -3824,6 +3985,10 @@ class WindowStateMixin:
|
||||
if not is_command and not self._property_target_changed(spec, text):
|
||||
self.statusBar().showMessage("请先在这一行的目标值列输入一个不同的新值。")
|
||||
return
|
||||
validation_error = "" if is_command else self._property_target_validation_error(spec, text)
|
||||
if validation_error:
|
||||
QMessageBox.information(self, "目标值无效", validation_error)
|
||||
return
|
||||
try:
|
||||
if not is_command:
|
||||
self._sync_property_edit_target(spec, text)
|
||||
|
||||
Reference in New Issue
Block a user