feat: 完善 Edge 一级编辑与 CAD 建模语义
This commit is contained in:
+7
-7
@@ -927,7 +927,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
object_edit_layout.setSpacing(4)
|
||||
self.property_table = QTableWidget(0, 5)
|
||||
self.property_table.setObjectName("propertyTable")
|
||||
self.property_table.setHorizontalHeaderLabels(["尺寸参数", "当前值", "修改方式", "目标值", "操作"])
|
||||
self.property_table.setHorizontalHeaderLabels(["尺寸参数", "当前值", "建模意图", "目标值", "操作"])
|
||||
self.property_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.property_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.property_table.setAlternatingRowColors(True)
|
||||
@@ -954,7 +954,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.property_table.setColumnWidth(4, 54)
|
||||
help_tip(
|
||||
self.property_table,
|
||||
"特征模式显示当前特征及局部关联特征的可变尺寸;输入目标值后,可按行应用或使用下方按钮应用当前修改。",
|
||||
"特征模式显示当前特征及局部关联特征的可变尺寸;建模意图决定这次修改是局部重建、拉伸/切除、端面移动还是整体缩放。",
|
||||
)
|
||||
self.property_table.itemChanged.connect(self._on_property_table_item_changed)
|
||||
object_edit_layout.addWidget(self.property_table)
|
||||
@@ -1150,12 +1150,12 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.chamfer_edge_button.clicked.connect(self.chamfer_edge)
|
||||
edit_layout.addWidget(self.chamfer_edge_button, 17, 0, 1, 2)
|
||||
|
||||
edit_layout.addWidget(QLabel("Edge目标长度"), 18, 0)
|
||||
edit_layout.addWidget(QLabel("Edge长度"), 18, 0)
|
||||
edge_length_inputs = QWidget()
|
||||
edge_length_layout = QHBoxLayout(edge_length_inputs)
|
||||
edge_length_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.edge_target_length_input = QLineEdit("")
|
||||
help_tip(self.edge_target_length_input, "当前Edge的目标长度。选中Edge后会自动填当前长度,改成新长度再执行。")
|
||||
help_tip(self.edge_target_length_input, "当前Edge的目标长度。选中Edge后会自动填当前长度,改成新长度后,还要确认右侧建模意图。")
|
||||
self.edge_start_x_input = QLineEdit("", edit_box)
|
||||
self.edge_start_y_input = QLineEdit("", edit_box)
|
||||
self.edge_start_z_input = QLineEdit("", edit_box)
|
||||
@@ -1192,7 +1192,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.edge_length_anchor_combo.setMinimumWidth(90)
|
||||
help_tip(
|
||||
self.edge_length_anchor_combo,
|
||||
"选择修改Edge长度时尽量固定哪里:自动默认固定起点并移动终点;中心会两端各动一半;固定起点/终点会明确控制局部形变端点。",
|
||||
"选择Edge长度修改的固定基准:自动默认固定起点并移动终点;中心会两端各动一半;固定起点/终点会明确控制局部形变端点。",
|
||||
)
|
||||
self.edge_length_strategy_combo = NoWheelComboBox()
|
||||
self.edge_length_strategy_combo.addItem("自动选择", "auto")
|
||||
@@ -1204,7 +1204,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.edge_length_strategy_combo.setMinimumWidth(130)
|
||||
help_tip(
|
||||
self.edge_length_strategy_combo,
|
||||
"选择Edge长度修改语义:自动会按可用性选择;只改当前Edge会让相邻面自然变斜;移动端面/保持垂直会让相关端面和同向边跟随;相邻圆柱直径会把圆Edge长度换算成孔/槽/凸台直径;缩放所属会让所属对象整体跟随变化。",
|
||||
"选择Edge长度修改的建模意图:自动会按可用性选择;只改当前Edge会让相邻面自然变斜;移动端面/保持垂直会让相关端面和同向边跟随;相邻圆柱直径会把圆Edge长度换算成孔/槽/凸台直径;缩放所属会让所属对象整体跟随变化。",
|
||||
)
|
||||
edge_length_layout.addWidget(self.edge_target_length_input, stretch=2)
|
||||
edge_length_layout.addWidget(self.edge_length_anchor_combo, stretch=1)
|
||||
@@ -1213,7 +1213,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.resize_edge_length_button = QPushButton("修改Edge长度")
|
||||
help_tip(
|
||||
self.resize_edge_length_button,
|
||||
"修改当前Edge长度。直线Edge优先局部形变;圆、椭圆和平面曲线会优先用局部/径向策略,必要时再用后备缩放。",
|
||||
"按已选择的建模意图修改当前Edge长度。直线Edge可只改当前边或移动端面;圆Edge可转成相邻圆柱直径;缩放所属会影响其它尺寸。",
|
||||
)
|
||||
self.resize_edge_length_button.clicked.connect(self.resize_any_edge_length)
|
||||
edit_layout.addWidget(self.resize_edge_length_button, 19, 0, 1, 2)
|
||||
|
||||
+285
-14
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
import time
|
||||
from typing import Callable, Iterable
|
||||
|
||||
from OCC.Core.BRep import BRep_Tool
|
||||
@@ -114,6 +115,10 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_first_level_topology_cache: dict[int, dict[str, object]] = {}
|
||||
self._cylindrical_first_level_topology_cache: dict[int, dict[str, object]] = {}
|
||||
self._face_first_level_fact_cache: dict[tuple[int, str], dict[str, object]] = {}
|
||||
self._edge_vertex_points_cache: dict[int, tuple[tuple[float, float, float], ...]] = {}
|
||||
self._edge_vertex_key_edge_ids_cache: dict[tuple[int, int, int, int, int], set[int]] | None = None
|
||||
self._edge_first_level_topology_cache: dict[int, dict[str, object]] = {}
|
||||
self._edge_first_level_fact_cache: dict[int, dict[str, object]] = {}
|
||||
self._local_face_deform_readiness_cache: dict[int, dict[str, object]] = {}
|
||||
self._edge_duplicate_key_ids_cache: dict[tuple[object, ...], list[int]] | None = None
|
||||
self._same_domain_internal_edge_ids_cache: set[int] | None = None
|
||||
@@ -198,6 +203,10 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_first_level_topology_cache.clear()
|
||||
self._cylindrical_first_level_topology_cache.clear()
|
||||
self._face_first_level_fact_cache.clear()
|
||||
self._edge_vertex_points_cache.clear()
|
||||
self._edge_vertex_key_edge_ids_cache = None
|
||||
self._edge_first_level_topology_cache.clear()
|
||||
self._edge_first_level_fact_cache.clear()
|
||||
self._local_face_deform_readiness_cache.clear()
|
||||
self._edge_duplicate_key_ids_cache = None
|
||||
self._same_domain_internal_edge_ids_cache = None
|
||||
@@ -904,6 +913,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
max_depth: int = 3,
|
||||
max_scan_faces: int = 72,
|
||||
max_features: int = 10,
|
||||
time_budget_seconds: float | None = None,
|
||||
lightweight: bool = False,
|
||||
) -> list[dict[str, object]]:
|
||||
"""Detect editable feature candidates near the selected face.
|
||||
|
||||
@@ -914,8 +925,73 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"""
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
raise ValueError(f"Unknown face id {face_id}")
|
||||
if max_features <= 0:
|
||||
return []
|
||||
|
||||
source_info = self.feature_info(face_id)
|
||||
deadline = None
|
||||
if time_budget_seconds is not None and time_budget_seconds > 0:
|
||||
deadline = time.monotonic() + float(time_budget_seconds)
|
||||
|
||||
def budget_expired() -> bool:
|
||||
return deadline is not None and time.monotonic() >= deadline
|
||||
|
||||
def selection_candidate_info(candidate_id: int) -> dict[str, object]:
|
||||
cached = self.cached_feature_info(candidate_id)
|
||||
if cached is not None:
|
||||
return cached
|
||||
info = self.quick_face_info(candidate_id)
|
||||
surface = str(info.get("surface", "") or "")
|
||||
result = dict(info)
|
||||
result.setdefault("kind", "feature")
|
||||
result.setdefault("feature_source_face_id", candidate_id)
|
||||
result.setdefault("feature_face_ids", (candidate_id,))
|
||||
result.setdefault("feature_highlight_face_ids", (candidate_id,))
|
||||
if surface == "cylinder":
|
||||
angular_span = _float_or_none(result.get("angular_span"))
|
||||
is_partial = (
|
||||
not _is_effectively_full_cylinder(result)
|
||||
and angular_span is not None
|
||||
and angular_span < math.tau * 0.92
|
||||
)
|
||||
if is_partial:
|
||||
result.setdefault("feature_type", "槽/半孔候选")
|
||||
result.setdefault("feature_guess", "cylindrical face")
|
||||
result.setdefault(
|
||||
"feature_edit_actions",
|
||||
"轻量探测阶段只读取半径、直径、轴线和大致高度;点击具体参数时会重新确认槽/半孔语义。",
|
||||
)
|
||||
else:
|
||||
result.setdefault("feature_type", "圆柱面候选")
|
||||
result.setdefault("feature_guess", "cylindrical face")
|
||||
result.setdefault(
|
||||
"feature_edit_actions",
|
||||
"轻量探测阶段只读取半径、直径、轴线和大致高度;点击具体参数时会重新确认孔、凸台或圆角语义。",
|
||||
)
|
||||
elif surface in {"cone", "sphere", "torus"}:
|
||||
result.setdefault("feature_type", f"{surface} 面候选")
|
||||
result.setdefault("feature_edit_actions", "轻量探测阶段只读取解析曲面参数;点击具体参数时会重新生成完整编辑计划。")
|
||||
elif surface == "plane":
|
||||
result["feature_type"] = "相邻平面 Face 候选"
|
||||
result.setdefault("feature_guess", "adjacent plane face")
|
||||
result.setdefault(
|
||||
"feature_edit_actions",
|
||||
"轻量探测阶段显示共享边的相邻平面 Face;点击具体参数时会切换到该 Face 并重新生成完整编辑计划。",
|
||||
)
|
||||
else:
|
||||
result.setdefault("feature_type", "相邻几何候选")
|
||||
result.setdefault("feature_edit_actions", "轻量探测阶段只显示相邻关系;暂不把它作为可修改特征。")
|
||||
result.setdefault(
|
||||
"feature_mode",
|
||||
"相邻特征轻量探测:为保证旋转、悬停和点选流畅,选择时只读取直接邻域的基础几何;完整识别留到执行具体修改时再计算。",
|
||||
)
|
||||
result["associated_feature_scan_mode"] = "lightweight"
|
||||
try:
|
||||
result.update(self._recognition_summary_fields(result))
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
source_info = selection_candidate_info(face_id) if lightweight else self.feature_info(face_id)
|
||||
source_area = max(float(source_info.get("area", 0.0) or 0.0), 1e-12)
|
||||
source_feature_faces = set(_int_values(source_info.get("feature_face_ids"))) or {face_id}
|
||||
visited = {face_id}
|
||||
@@ -923,12 +999,16 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
candidate_hops: dict[int, int] = {}
|
||||
|
||||
while frontier and len(visited) < max_scan_faces:
|
||||
if budget_expired():
|
||||
break
|
||||
current_id, depth = frontier.pop(0)
|
||||
if depth >= max_depth:
|
||||
continue
|
||||
edge_ids = self._face_boundary_edge_ids(current_id)
|
||||
neighbors = sorted(set(self._adjacent_face_ids_for_edges(edge_ids, current_id)) - {current_id})
|
||||
for neighbor_id in neighbors:
|
||||
if budget_expired():
|
||||
break
|
||||
candidate_hops[neighbor_id] = min(candidate_hops.get(neighbor_id, depth + 1), depth + 1)
|
||||
if neighbor_id in visited or len(visited) >= max_scan_faces:
|
||||
continue
|
||||
@@ -946,28 +1026,38 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
results: list[dict[str, object]] = []
|
||||
seen_features: set[tuple[str, frozenset[int]]] = set()
|
||||
for candidate_id, hop_count in sorted(candidate_hops.items(), key=lambda item: (item[1], item[0])):
|
||||
if budget_expired():
|
||||
break
|
||||
if candidate_id in source_feature_faces:
|
||||
continue
|
||||
try:
|
||||
info = self.feature_info(candidate_id)
|
||||
info = selection_candidate_info(candidate_id) if lightweight else self.feature_info(candidate_id)
|
||||
except Exception:
|
||||
continue
|
||||
surface = str(info.get("surface", "") or "")
|
||||
feature_guess = str(info.get("feature_guess", "") or "")
|
||||
feature_type = str(info.get("feature_type", "") or "")
|
||||
is_semantic = bool(
|
||||
info.get("prismatic_extrusion_status") == "candidate"
|
||||
or surface in {"cone", "sphere", "torus"}
|
||||
or (
|
||||
surface == "cylinder"
|
||||
and feature_guess
|
||||
in {
|
||||
"hole/groove candidate",
|
||||
"boss/outer-round candidate",
|
||||
"round/fillet candidate",
|
||||
}
|
||||
if lightweight:
|
||||
is_semantic = bool(
|
||||
info.get("prismatic_extrusion_status") == "candidate"
|
||||
or surface in {"plane", "cylinder", "cone", "sphere", "torus"}
|
||||
)
|
||||
if surface == "plane" and hop_count > 1:
|
||||
is_semantic = False
|
||||
else:
|
||||
is_semantic = bool(
|
||||
info.get("prismatic_extrusion_status") == "candidate"
|
||||
or surface in {"cone", "sphere", "torus"}
|
||||
or (
|
||||
surface == "cylinder"
|
||||
and feature_guess
|
||||
in {
|
||||
"hole/groove candidate",
|
||||
"boss/outer-round candidate",
|
||||
"round/fillet candidate",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
if not is_semantic:
|
||||
continue
|
||||
identity_face_ids = _int_values(info.get("feature_face_ids"))
|
||||
@@ -982,11 +1072,15 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
continue
|
||||
seen_features.add(identity)
|
||||
related = dict(info)
|
||||
association_label = ""
|
||||
if lightweight and surface == "plane":
|
||||
association_label = f"相邻平面 Face {candidate_id}"
|
||||
related.update(
|
||||
{
|
||||
"association_source_face_id": candidate_id,
|
||||
"association_hop_count": hop_count,
|
||||
"association_relation": "shared-edge-topology",
|
||||
"association_label": association_label,
|
||||
"association_priority": (
|
||||
0 if surface == "cylinder" else (1 if surface in {"cone", "sphere", "torus"} else 2)
|
||||
),
|
||||
@@ -2869,6 +2963,183 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
matches.append(face_id)
|
||||
return matches
|
||||
|
||||
def _edge_vertex_points(self, edge_id: int) -> tuple[tuple[float, float, float], ...]:
|
||||
if edge_id < 0 or edge_id >= len(self.edges):
|
||||
return ()
|
||||
cached = self._edge_vertex_points_cache.get(edge_id)
|
||||
if cached is not None:
|
||||
return tuple(cached)
|
||||
|
||||
diagonal = _shape_diagonal(self.shape)
|
||||
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
|
||||
points_by_key: dict[tuple[int, int, int], tuple[float, float, float]] = {}
|
||||
explorer = TopExp_Explorer(self.edges[edge_id], TopAbs_VERTEX)
|
||||
while explorer.More():
|
||||
vertex = topods.Vertex(explorer.Current())
|
||||
point = _point_tuple(BRep_Tool.Pnt(vertex))
|
||||
points_by_key[self._local_point_key(point, tolerance)] = point
|
||||
explorer.Next()
|
||||
points = tuple(points_by_key[key] for key in sorted(points_by_key))
|
||||
self._edge_vertex_points_cache[edge_id] = points
|
||||
return points
|
||||
|
||||
def _edge_vertex_key_edge_ids(self) -> dict[tuple[int, int, int, int, int], set[int]]:
|
||||
cached = self._edge_vertex_key_edge_ids_cache
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
diagonal = _shape_diagonal(self.shape)
|
||||
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
|
||||
mapping: dict[tuple[int, int, int, int, int], set[int]] = {}
|
||||
for edge_id in range(len(self.edges)):
|
||||
part_id = self.edge_part_ids[edge_id] if edge_id < len(self.edge_part_ids) else -1
|
||||
solid_id = self._edge_solid_id(edge_id)
|
||||
for point in self._edge_vertex_points(edge_id):
|
||||
key = (int(part_id), int(solid_id), *self._local_point_key(point, tolerance))
|
||||
mapping.setdefault(key, set()).add(edge_id)
|
||||
self._edge_vertex_key_edge_ids_cache = mapping
|
||||
return mapping
|
||||
|
||||
def _edge_shared_vertex_adjacent_edge_ids(self, edge_id: int) -> tuple[int, ...]:
|
||||
if edge_id < 0 or edge_id >= len(self.edges):
|
||||
return ()
|
||||
diagonal = _shape_diagonal(self.shape)
|
||||
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
|
||||
part_id = self.edge_part_ids[edge_id] if edge_id < len(self.edge_part_ids) else -1
|
||||
solid_id = self._edge_solid_id(edge_id)
|
||||
mapping = self._edge_vertex_key_edge_ids()
|
||||
adjacent: set[int] = set()
|
||||
for point in self._edge_vertex_points(edge_id):
|
||||
key = (int(part_id), int(solid_id), *self._local_point_key(point, tolerance))
|
||||
adjacent.update(mapping.get(key, set()))
|
||||
adjacent.discard(edge_id)
|
||||
return tuple(sorted(item for item in adjacent if 0 <= item < len(self.edges)))
|
||||
|
||||
def edge_first_level_topology(self, edge_id: int) -> dict[str, object]:
|
||||
"""Return the explicit first-level B-Rep neighborhood for an Edge."""
|
||||
if edge_id < 0 or edge_id >= len(self.edges):
|
||||
raise ValueError(f"Unknown edge id {edge_id}")
|
||||
cached = self._edge_first_level_topology_cache.get(edge_id)
|
||||
if cached is not None:
|
||||
return dict(cached)
|
||||
|
||||
source_part_id = self.edge_part_ids[edge_id] if edge_id < len(self.edge_part_ids) else -1
|
||||
source_solid_id = self._edge_solid_id(edge_id)
|
||||
endpoint_points = self._edge_vertex_points(edge_id)
|
||||
adjacent_edge_ids = self._edge_shared_vertex_adjacent_edge_ids(edge_id)
|
||||
adjacent_face_ids = tuple(self._edge_adjacent_face_ids(edge_id))
|
||||
adjacent_surface_types = tuple((face_id, self.face_surface_kind(face_id)) for face_id in adjacent_face_ids)
|
||||
|
||||
adjacent_face_boundary_edges: set[int] = set()
|
||||
shared_edges_by_face: list[dict[str, object]] = []
|
||||
for face_id in adjacent_face_ids:
|
||||
boundary_edge_ids = tuple(self._face_boundary_edge_ids(face_id))
|
||||
adjacent_face_boundary_edges.update(boundary_edge_ids)
|
||||
shared_edges_by_face.append(
|
||||
{
|
||||
"face_id": face_id,
|
||||
"edge_ids": (edge_id,),
|
||||
"edge_count": 1,
|
||||
"surface": self.face_surface_kind(face_id),
|
||||
"face_boundary_edge_ids": boundary_edge_ids,
|
||||
}
|
||||
)
|
||||
|
||||
first_level_edge_ids = tuple(sorted({edge_id, *adjacent_edge_ids}))
|
||||
topology = {
|
||||
"topology_relation_model": "STEP/B-Rep edge first-level",
|
||||
"topology_relation_depth": 1,
|
||||
"topology_relation_scope": "selected Edge + endpoint Vertices + shared-endpoint Edges + direct incident Faces",
|
||||
"topology_relation_boundary": "shared-vertex/shared-face",
|
||||
"topology_ignored_relation_depths": ("second-level", "third-level", "deeper"),
|
||||
"topology_ignored_relation_note": (
|
||||
"Edge first-level topology stops at endpoint Vertices, shared-endpoint Edges, "
|
||||
"and Faces that directly use the selected Edge. Edges/Faces reached through those Faces "
|
||||
"are second-level or deeper and are not propagated automatically in this stage."
|
||||
),
|
||||
"source_edge_id": edge_id,
|
||||
"source_part_id": source_part_id,
|
||||
"source_solid_id": source_solid_id,
|
||||
"selected_edge_id": edge_id,
|
||||
"selected_edge_ids": (edge_id,),
|
||||
"selected_edge_count": 1,
|
||||
"first_level_vertex_points": endpoint_points,
|
||||
"first_level_vertex_count": len(endpoint_points),
|
||||
"first_level_adjacent_edge_ids": adjacent_edge_ids,
|
||||
"first_level_adjacent_edge_count": len(adjacent_edge_ids),
|
||||
"first_level_edge_ids": first_level_edge_ids,
|
||||
"first_level_edge_count": len(first_level_edge_ids),
|
||||
"first_level_adjacent_face_ids": adjacent_face_ids,
|
||||
"first_level_adjacent_face_count": len(adjacent_face_ids),
|
||||
"first_level_adjacent_surface_types": adjacent_surface_types,
|
||||
"first_level_shared_edges_by_face": tuple(shared_edges_by_face),
|
||||
"first_level_adjacent_face_boundary_edge_ids": tuple(sorted(adjacent_face_boundary_edges)),
|
||||
"first_level_adjacent_face_boundary_edge_count": len(adjacent_face_boundary_edges),
|
||||
"first_level_topology_note": (
|
||||
f"Selected Edge 1, endpoint Vertex {len(endpoint_points)}, "
|
||||
f"shared-endpoint Edge {len(adjacent_edge_ids)}, direct adjacent Face {len(adjacent_face_ids)}. "
|
||||
"Second-level and deeper propagation is not automatic in this stage."
|
||||
),
|
||||
}
|
||||
self._edge_first_level_topology_cache[edge_id] = dict(topology)
|
||||
return dict(topology)
|
||||
|
||||
def edge_first_level_facts(self, edge_id: int) -> dict[str, object]:
|
||||
"""Return a unified first-level fact graph for Edge recognition and edit plans."""
|
||||
if edge_id < 0 or edge_id >= len(self.edges):
|
||||
raise ValueError(f"Unknown edge id {edge_id}")
|
||||
cached = self._edge_first_level_fact_cache.get(edge_id)
|
||||
if cached is not None:
|
||||
return dict(cached)
|
||||
|
||||
topology = self.edge_first_level_topology(edge_id)
|
||||
endpoint_points = tuple(topology.get("first_level_vertex_points") or ())
|
||||
adjacent_edge_ids = tuple(_int_values(topology.get("first_level_adjacent_edge_ids")))
|
||||
adjacent_face_ids = tuple(_int_values(topology.get("first_level_adjacent_face_ids")))
|
||||
included_edge_ids = tuple(_int_values(topology.get("first_level_edge_ids"))) or (edge_id,)
|
||||
ignored_depths = tuple(topology.get("topology_ignored_relation_depths") or ("second-level", "third-level"))
|
||||
role_groups = (
|
||||
{"role": "selected-edge", "edge_ids": (edge_id,), "count": 1},
|
||||
{"role": "endpoint-vertices", "vertex_points": endpoint_points, "count": len(endpoint_points)},
|
||||
{"role": "shared-endpoint-edges", "edge_ids": adjacent_edge_ids, "count": len(adjacent_edge_ids)},
|
||||
{"role": "direct-incident-faces", "face_ids": adjacent_face_ids, "count": len(adjacent_face_ids)},
|
||||
)
|
||||
facts = {
|
||||
"first_level_fact_model": "STEP/B-Rep first-level fact graph",
|
||||
"first_level_fact_source_model": "edge",
|
||||
"first_level_fact_status": "ready",
|
||||
"first_level_fact_relation_depth": 1,
|
||||
"first_level_fact_relation_boundary": "shared-vertex/shared-face",
|
||||
"first_level_fact_scope": "edge",
|
||||
"first_level_fact_subject_role": "selected Edge",
|
||||
"first_level_fact_subject_edge_ids": (edge_id,),
|
||||
"first_level_fact_subject_edge_count": 1,
|
||||
"first_level_fact_boundary_edge_ids": (edge_id,),
|
||||
"first_level_fact_boundary_edge_count": 1,
|
||||
"first_level_fact_boundary_vertex_points": endpoint_points,
|
||||
"first_level_fact_boundary_vertex_count": len(endpoint_points),
|
||||
"first_level_fact_adjacent_edge_ids": adjacent_edge_ids,
|
||||
"first_level_fact_adjacent_edge_count": len(adjacent_edge_ids),
|
||||
"first_level_fact_adjacent_face_ids": adjacent_face_ids,
|
||||
"first_level_fact_adjacent_face_count": len(adjacent_face_ids),
|
||||
"first_level_fact_adjacent_surface_types": tuple(topology.get("first_level_adjacent_surface_types") or ()),
|
||||
"first_level_fact_shared_edges_by_face": tuple(topology.get("first_level_shared_edges_by_face") or ()),
|
||||
"first_level_fact_included_edge_ids": included_edge_ids,
|
||||
"first_level_fact_included_edge_count": len(included_edge_ids),
|
||||
"first_level_fact_included_face_ids": adjacent_face_ids,
|
||||
"first_level_fact_included_face_count": len(adjacent_face_ids),
|
||||
"first_level_fact_role_groups": role_groups,
|
||||
"first_level_fact_ignored_relation_depths": ignored_depths,
|
||||
"first_level_fact_ignored_relation_note": topology.get("topology_ignored_relation_note", ""),
|
||||
"first_level_fact_summary": (
|
||||
f"selected Edge 1, endpoint Vertex {len(endpoint_points)}, "
|
||||
f"shared-endpoint Edge {len(adjacent_edge_ids)}, direct adjacent Face {len(adjacent_face_ids)}; "
|
||||
"deeper relations are recorded as future propagation targets, not edited automatically."
|
||||
),
|
||||
}
|
||||
self._edge_first_level_fact_cache[edge_id] = dict(facts)
|
||||
return dict(facts)
|
||||
|
||||
def edge_info(self, edge_id: int) -> dict[str, object]:
|
||||
if edge_id in self._edge_info_cache:
|
||||
return dict(self._edge_info_cache[edge_id])
|
||||
|
||||
+342
-105
@@ -49,8 +49,10 @@ from OCC.Core.GeomAbs import (
|
||||
)
|
||||
from OCC.Core.GProp import GProp_GProps
|
||||
from OCC.Core.GC import GC_MakeArcOfCircle
|
||||
from OCC.Core.GeomAPI import GeomAPI_PointsToBSplineSurface
|
||||
from OCC.Core.ShapeFix import ShapeFix_Shape
|
||||
from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain
|
||||
from OCC.Core.TColgp import TColgp_Array2OfPnt
|
||||
from OCC.Core.TopAbs import (
|
||||
TopAbs_EDGE,
|
||||
TopAbs_EXTERNAL,
|
||||
@@ -342,6 +344,28 @@ class OperationMixin:
|
||||
f"edges {before_part_stats.edges}->{after_part_stats.edges}."
|
||||
)
|
||||
|
||||
def _edge_first_level_plan_fields(self, edge_id: int) -> dict[str, object]:
|
||||
try:
|
||||
return self.edge_first_level_facts(edge_id)
|
||||
except Exception as exc:
|
||||
return {
|
||||
"first_level_fact_model": "STEP/B-Rep first-level fact graph",
|
||||
"first_level_fact_source_model": "edge",
|
||||
"first_level_fact_status": "unavailable",
|
||||
"first_level_fact_relation_depth": 1,
|
||||
"first_level_fact_relation_boundary": "shared-vertex/shared-face",
|
||||
"first_level_fact_scope": "edge",
|
||||
"first_level_fact_subject_role": "selected Edge",
|
||||
"first_level_fact_subject_edge_ids": (edge_id,),
|
||||
"first_level_fact_subject_edge_count": 1,
|
||||
"first_level_fact_boundary_edge_count": 1,
|
||||
"first_level_fact_boundary_vertex_count": 0,
|
||||
"first_level_fact_adjacent_edge_count": 0,
|
||||
"first_level_fact_adjacent_face_count": 0,
|
||||
"first_level_fact_ignored_relation_depths": ("second-level", "third-level", "deeper"),
|
||||
"first_level_fact_summary": f"Edge first-level facts are temporarily unavailable: {exc}",
|
||||
}
|
||||
|
||||
def edge_fillet_plan(self, edge_id: int, radius: float) -> dict[str, object]:
|
||||
if edge_id < 0 or edge_id >= len(self.edges):
|
||||
raise ValueError(f"Unknown edge id {edge_id}")
|
||||
@@ -366,7 +390,7 @@ class OperationMixin:
|
||||
|
||||
length = float(info.get("length", 0.0))
|
||||
radius_to_length_ratio = radius / max(length, 1e-9)
|
||||
return {
|
||||
plan = {
|
||||
"status": readiness["fillet_status"],
|
||||
"risk": readiness["fillet_risk"],
|
||||
"message": readiness["fillet_note"],
|
||||
@@ -391,6 +415,8 @@ class OperationMixin:
|
||||
"不是修改已有圆角面。"
|
||||
),
|
||||
}
|
||||
plan.update(self._edge_first_level_plan_fields(edge_id))
|
||||
return plan
|
||||
|
||||
def edge_chamfer_plan(self, edge_id: int, distance: float) -> dict[str, object]:
|
||||
if edge_id < 0 or edge_id >= len(self.edges):
|
||||
@@ -416,7 +442,7 @@ class OperationMixin:
|
||||
|
||||
length = float(info.get("length", 0.0))
|
||||
distance_to_length_ratio = distance / max(length, 1e-9)
|
||||
return {
|
||||
plan = {
|
||||
"status": readiness["chamfer_status"],
|
||||
"risk": readiness["chamfer_risk"],
|
||||
"message": readiness["chamfer_note"],
|
||||
@@ -440,6 +466,8 @@ class OperationMixin:
|
||||
"在当前直线 Edge 及其相邻 Face 上调用 OCCT 对称倒角;会替换这条边附近的局部拓扑。"
|
||||
),
|
||||
}
|
||||
plan.update(self._edge_first_level_plan_fields(edge_id))
|
||||
return plan
|
||||
|
||||
def edge_asymmetric_chamfer_plan(
|
||||
self,
|
||||
@@ -669,6 +697,7 @@ class OperationMixin:
|
||||
"end_point": info.get("end_point"),
|
||||
"length_center": info.get("length_center"),
|
||||
}
|
||||
base.update(self._edge_first_level_plan_fields(edge_id))
|
||||
warnings: list[str] = [
|
||||
"Edge长度修改基于当前 STEP/B-Rep 结果几何,不是 CAD 建模历史里的参数编辑。"
|
||||
]
|
||||
@@ -693,7 +722,7 @@ class OperationMixin:
|
||||
warnings.append("长度变化超过当前Edge长度的 25%,请确认预览范围。")
|
||||
|
||||
if not blockers and force_local and curve != "line":
|
||||
blockers.append("“只变当前Edge”策略当前只支持直线Edge。")
|
||||
blockers.append("“只改当前Edge”策略当前只支持直线Edge。")
|
||||
if not blockers and force_end_face and curve != "line":
|
||||
blockers.append("“移动端面/整体尺寸”策略当前只支持直线Edge。")
|
||||
if not blockers and force_cylinder and curve != "circle":
|
||||
@@ -711,7 +740,7 @@ class OperationMixin:
|
||||
risk = _max_risk(risk, str(local_candidate.get("local_edge_deform_risk", "medium")))
|
||||
status = "caution" if risk != "low" else status
|
||||
warnings.append(
|
||||
"将优先只移动当前 Edge 的端点并重建相邻平面;非共面的四边面会拆成三角面。"
|
||||
"将优先只移动当前 Edge 的端点并重建相邻平面;四点非共面面会优先重建为连续曲面。"
|
||||
)
|
||||
base.update(local_candidate)
|
||||
elif local_skip_note:
|
||||
@@ -945,7 +974,7 @@ class OperationMixin:
|
||||
"local_edge_deform_moved_endpoint_count": 2 if anchor_mode == "center" else 1,
|
||||
"local_edge_deform_risk": local_risk,
|
||||
"local_edge_deform_note": (
|
||||
"只移动当前 Edge 的端点并重建所属平面多面体;非共面 Face 会被拆成三角面。"
|
||||
"只移动当前 Edge 的端点并重建所属平面多面体;四点非共面 Face 会优先重建为连续曲面。"
|
||||
),
|
||||
"part_solid_count": part_solid_count,
|
||||
},
|
||||
@@ -999,6 +1028,7 @@ class OperationMixin:
|
||||
"length_center": info.get("length_center"),
|
||||
"resize_strategy": "local-edge-endpoint-deform",
|
||||
}
|
||||
base.update(self._edge_first_level_plan_fields(edge_id))
|
||||
warnings = [
|
||||
"Edge endpoint coordinate edit rebuilds the current STEP/B-Rep result geometry; it is not recovered CAD history."
|
||||
]
|
||||
@@ -1082,7 +1112,7 @@ class OperationMixin:
|
||||
)
|
||||
risk = _max_risk(risk, str(local_candidate.get("local_edge_deform_risk", "medium")))
|
||||
warnings.append(
|
||||
"The selected Edge endpoint will be moved and the surrounding planar faces will be rebuilt; non-planar faces may be split into triangles."
|
||||
"The selected Edge endpoint will be moved and the surrounding planar faces will be rebuilt; four-point non-planar faces are rebuilt as continuous surfaces when possible."
|
||||
)
|
||||
|
||||
if blockers:
|
||||
@@ -1136,6 +1166,7 @@ class OperationMixin:
|
||||
"target_edge_center": target,
|
||||
"resize_strategy": "local-edge-center-deform",
|
||||
}
|
||||
base.update(self._edge_first_level_plan_fields(edge_id))
|
||||
warnings = [
|
||||
"Edge center coordinate edit rebuilds the current STEP/B-Rep result geometry; it is not recovered CAD history."
|
||||
]
|
||||
@@ -1205,7 +1236,7 @@ class OperationMixin:
|
||||
)
|
||||
risk = _max_risk(risk, str(local_candidate.get("local_edge_deform_risk", "medium")))
|
||||
warnings.append(
|
||||
"Both endpoints of the selected Edge will be moved and the surrounding planar faces will be rebuilt; non-planar faces may be split into triangles."
|
||||
"Both endpoints of the selected Edge will be moved and the surrounding planar faces will be rebuilt; four-point non-planar faces are rebuilt as continuous surfaces when possible."
|
||||
)
|
||||
|
||||
if blockers:
|
||||
@@ -1324,7 +1355,7 @@ class OperationMixin:
|
||||
if strategy == "local-edge-only-deform":
|
||||
return (
|
||||
"只改当前Edge:只移动被选Edge的端点并重建相邻平面;相邻面会自然变斜,"
|
||||
"必要时非共面四边面会拆成三角面,整体端面不会一起平移。"
|
||||
"四点非共面面会优先重建为连续曲面,整体端面不会一起平移。"
|
||||
)
|
||||
if strategy == "move-edge-end-plane-by-push-pull":
|
||||
end_face = plan.get("end_face_id")
|
||||
@@ -1557,6 +1588,7 @@ class OperationMixin:
|
||||
}
|
||||
if candidate:
|
||||
result.update(candidate)
|
||||
result.update(self._edge_first_level_plan_fields(edge_id))
|
||||
return result
|
||||
|
||||
def _circular_edge_axis_move_cylinder_candidate(
|
||||
@@ -2019,7 +2051,7 @@ class OperationMixin:
|
||||
warnings.append(f"目标{axis_label}变化超过当前值的 50%,周边几何变形或修复失败的概率较高。")
|
||||
elif delta_ratio > 0.25:
|
||||
risk = _max_risk(risk, "medium")
|
||||
warnings.append(f"目标{axis_label}变化超过当前值的 25%,请确认影响范围。")
|
||||
warnings.append(f"目标{axis_label}变化超过当前值的 25%,请确认建模意图。")
|
||||
|
||||
part_id = int(info.get("part_id", -1))
|
||||
solid_id = int(info.get("solid_id", -1))
|
||||
@@ -2042,7 +2074,7 @@ class OperationMixin:
|
||||
|
||||
target_major = target_radius if axis_kind == "major" else info.get("major_radius")
|
||||
target_minor = target_radius if axis_kind == "minor" else info.get("minor_radius")
|
||||
return {
|
||||
plan = {
|
||||
"edge_id": edge_id,
|
||||
"part_id": part_id,
|
||||
"solid_id": solid_id,
|
||||
@@ -2081,6 +2113,8 @@ class OperationMixin:
|
||||
"affine_target_kind": target_kind,
|
||||
"part_solid_count": part_solid_count,
|
||||
}
|
||||
plan.update(self._edge_first_level_plan_fields(edge_id))
|
||||
return plan
|
||||
|
||||
def resize_ellipse_edge_axis_radius(
|
||||
self,
|
||||
@@ -2857,7 +2891,7 @@ class OperationMixin:
|
||||
except (TypeError, ValueError):
|
||||
distance = 0.0
|
||||
frame = None
|
||||
blockers = ["目标偏移曲面必须是数字。"]
|
||||
blockers = ["目标偏移变换位置必须是数字。"]
|
||||
else:
|
||||
frame = self.face_plane_offset_frame(face_id)
|
||||
blockers = []
|
||||
@@ -2865,7 +2899,7 @@ class OperationMixin:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
blockers.append(f"Unknown face id {face_id}")
|
||||
if frame is None:
|
||||
blockers.append("当前 Face 缺少稳定平面方向或基准点,不能执行偏移曲面(局部重建)。")
|
||||
blockers.append("当前 Face 缺少稳定平面方向或基准点,不能执行偏移变换(局部重建)。")
|
||||
|
||||
current_center = None
|
||||
target_center = None
|
||||
@@ -2905,8 +2939,8 @@ class OperationMixin:
|
||||
"target_face_center": target_center,
|
||||
"face_center_move_vector": move_vector,
|
||||
"resize_strategy": "local-face-plane-offset-deform",
|
||||
"edit_strategy_label": "偏移曲面(局部重建)",
|
||||
"edit_semantics": "把目标偏移曲面换算成沿当前面垂直方向的移动量,移动当前 Face 并重建相邻平面。",
|
||||
"edit_strategy_label": "偏移变换(局部重建)",
|
||||
"edit_semantics": "把目标偏移变换位置换算成沿当前面垂直方向的移动量,移动当前 Face 并重建相邻平面。",
|
||||
}
|
||||
|
||||
plan = self.face_center_local_move_plan(face_id, target_center)
|
||||
@@ -2918,9 +2952,9 @@ class OperationMixin:
|
||||
"plane_direction": plane_direction,
|
||||
"plane_offset_distance": distance,
|
||||
"resize_strategy": "local-face-plane-offset-deform",
|
||||
"edit_strategy_label": "偏移曲面(局部重建)",
|
||||
"edit_strategy_label": "偏移变换(局部重建)",
|
||||
"edit_semantics": (
|
||||
"把目标偏移曲面换算成沿当前面垂直方向的移动量,移动当前 Face 的顶点并重建相邻平面;"
|
||||
"把目标偏移变换位置换算成沿当前面垂直方向的移动量,移动当前 Face 的顶点并重建相邻平面;"
|
||||
"不拉伸/切除加料/切削,也不平移所属对象。"
|
||||
),
|
||||
}
|
||||
@@ -2933,7 +2967,7 @@ class OperationMixin:
|
||||
except (TypeError, ValueError):
|
||||
distance = 0.0
|
||||
frame = None
|
||||
blockers = ["目标偏移曲面必须是数字。"]
|
||||
blockers = ["目标偏移变换位置必须是数字。"]
|
||||
else:
|
||||
frame = self.face_plane_offset_frame(face_id)
|
||||
blockers = []
|
||||
@@ -2944,7 +2978,7 @@ class OperationMixin:
|
||||
else:
|
||||
info = self.face_info(face_id)
|
||||
if frame is None:
|
||||
blockers.append("当前 Face 缺少稳定平面方向或基准点,不能按偏移曲面平移所属对象。")
|
||||
blockers.append("当前 Face 缺少稳定平面方向或基准点,不能按偏移变换平移所属对象。")
|
||||
|
||||
plane_origin = None
|
||||
plane_direction = None
|
||||
@@ -2987,8 +3021,8 @@ class OperationMixin:
|
||||
"translation_vector": vector,
|
||||
"resize_strategy": "translate-owning-shape-from-plane-offset",
|
||||
"translate_strategy": f"translate-{target_kind}",
|
||||
"edit_strategy_label": "按偏移曲面平移所属对象",
|
||||
"edit_semantics": "把目标偏移曲面换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid。",
|
||||
"edit_strategy_label": "按偏移变换平移所属对象",
|
||||
"edit_semantics": "把目标偏移变换位置换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid。",
|
||||
}
|
||||
|
||||
plan = self.translate_solid_plan(solid_id, vector) if target_kind == "solid" else self.translate_part_plan(part_id, vector)
|
||||
@@ -2997,7 +3031,7 @@ class OperationMixin:
|
||||
if diagonal > 1e-9:
|
||||
distance_ratio = abs(distance) / diagonal
|
||||
if distance_ratio > 5.0:
|
||||
blocker = "目标偏移曲面需要移动的距离超过所属对象尺寸的 5 倍,容易把特征移动到远离模型的位置。"
|
||||
blocker = "目标偏移变换位置需要移动的距离超过所属对象尺寸的 5 倍,容易把特征移动到远离模型的位置。"
|
||||
blockers = [part for part in str(plan.get("blockers") or "").split(";") if part]
|
||||
blockers.append(blocker)
|
||||
plan["status"] = "blocked"
|
||||
@@ -3017,8 +3051,8 @@ class OperationMixin:
|
||||
"face_offset_distance_ratio": distance_ratio,
|
||||
"resize_strategy": "translate-owning-shape-from-plane-offset",
|
||||
"translate_strategy": f"translate-{target_kind}",
|
||||
"edit_strategy_label": "按偏移曲面平移所属对象",
|
||||
"edit_semantics": "把目标偏移曲面换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。",
|
||||
"edit_strategy_label": "按偏移变换平移所属对象",
|
||||
"edit_semantics": "把目标偏移变换位置换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。",
|
||||
}
|
||||
)
|
||||
return plan
|
||||
@@ -3466,7 +3500,7 @@ class OperationMixin:
|
||||
for face in solid_faces:
|
||||
surf = BRepAdaptor_Surface(face)
|
||||
if surf.GetType() != GeomAbs_Plane:
|
||||
blockers.append("局部 Face 移动暂只支持全平面实体;含曲面的模型请使用其它编辑方式。")
|
||||
blockers.append("局部 Face 移动暂只支持全平面实体;含曲面的模型请使用其它建模意图。")
|
||||
break
|
||||
if len(_explore(face, TopAbs_WIRE)) != 1:
|
||||
blockers.append("局部 Face 移动暂不处理带内孔的 Face;请使用孔/槽专门入口。")
|
||||
@@ -6762,67 +6796,94 @@ class OperationMixin:
|
||||
)
|
||||
if plan["status"] == "blocked":
|
||||
raise ValueError(str(plan["message"]))
|
||||
part_id = int(plan.get("part_id", -1))
|
||||
part = self.part_by_id(part_id) if part_id >= 0 else None
|
||||
old_part_shape = part.shape if part is not None else None
|
||||
if plan.get("resize_strategy") == "local-edge-only-deform":
|
||||
self._apply_local_edge_deform(plan)
|
||||
result_check = self._edge_length_result_summary(plan)
|
||||
return (
|
||||
"Edge length resize completed by local edge-only deformation: "
|
||||
f"edge {edge_id}, current_length={float(plan['current_length']):g}, "
|
||||
f"target_length={float(plan['target_length']):g}, "
|
||||
f"delta={float(plan['delta_length']):g}, "
|
||||
f"anchor={plan.get('local_edge_deform_anchor')}, "
|
||||
f"rebuilt_faces={plan.get('local_edge_deform_face_count')}, "
|
||||
f"target={plan.get('local_edge_deform_target_kind')}, "
|
||||
f"risk={plan['risk']}. {result_check}"
|
||||
)
|
||||
try:
|
||||
self._apply_local_edge_deform(plan)
|
||||
result_check = self._edge_length_result_summary_or_raise(plan)
|
||||
return (
|
||||
"Edge length resize completed by local edge-only deformation: "
|
||||
f"edge {edge_id}, current_length={float(plan['current_length']):g}, "
|
||||
f"target_length={float(plan['target_length']):g}, "
|
||||
f"delta={float(plan['delta_length']):g}, "
|
||||
f"anchor={plan.get('local_edge_deform_anchor')}, "
|
||||
f"rebuilt_faces={plan.get('local_edge_deform_face_count')}, "
|
||||
f"target={plan.get('local_edge_deform_target_kind')}, "
|
||||
f"risk={plan['risk']}. {result_check}"
|
||||
)
|
||||
except Exception:
|
||||
if part is not None and old_part_shape is not None:
|
||||
part.shape = old_part_shape
|
||||
self.refresh_topology()
|
||||
raise
|
||||
if plan.get("resize_strategy") == "move-edge-end-plane-by-push-pull":
|
||||
push_result = self.push_pull_face(int(plan["end_face_id"]), float(plan["push_pull_distance"]))
|
||||
result_check = self._edge_length_result_summary(plan)
|
||||
return (
|
||||
"Edge length resize completed by end-face push/pull: "
|
||||
f"edge {edge_id}, current_length={float(plan['current_length']):g}, "
|
||||
f"target_length={float(plan['target_length']):g}, "
|
||||
f"delta={float(plan['delta_length']):g}, "
|
||||
f"end_face={int(plan['end_face_id'])}, "
|
||||
f"push_pull_distance={float(plan['push_pull_distance']):g}, "
|
||||
f"anchor={plan.get('edge_length_anchor_label')}, "
|
||||
f"risk={plan['risk']}. {result_check} {push_result}"
|
||||
)
|
||||
try:
|
||||
push_result = self.push_pull_face(int(plan["end_face_id"]), float(plan["push_pull_distance"]))
|
||||
result_check = self._edge_length_result_summary_or_raise(plan)
|
||||
return (
|
||||
"Edge length resize completed by end-face push/pull: "
|
||||
f"edge {edge_id}, current_length={float(plan['current_length']):g}, "
|
||||
f"target_length={float(plan['target_length']):g}, "
|
||||
f"delta={float(plan['delta_length']):g}, "
|
||||
f"end_face={int(plan['end_face_id'])}, "
|
||||
f"push_pull_distance={float(plan['push_pull_distance']):g}, "
|
||||
f"anchor={plan.get('edge_length_anchor_label')}, "
|
||||
f"risk={plan['risk']}. {result_check} {push_result}"
|
||||
)
|
||||
except Exception:
|
||||
if part is not None and old_part_shape is not None:
|
||||
part.shape = old_part_shape
|
||||
self.refresh_topology()
|
||||
raise
|
||||
|
||||
if plan.get("resize_strategy") == "resize-adjacent-cylinder-from-circular-edge-length":
|
||||
face_id = int(plan["cylinder_resize_face_id"])
|
||||
target_diameter = float(plan["cylinder_resize_target_diameter"])
|
||||
if plan.get("circular_edge_cylinder_mode") == "boss":
|
||||
resize_result = self.resize_cylindrical_boss(face_id, target_diameter)
|
||||
else:
|
||||
resize_result = self.resize_cylindrical_hole(face_id, target_diameter)
|
||||
result_check = self._edge_length_result_summary(plan)
|
||||
try:
|
||||
face_id = int(plan["cylinder_resize_face_id"])
|
||||
target_diameter = float(plan["cylinder_resize_target_diameter"])
|
||||
if plan.get("circular_edge_cylinder_mode") == "boss":
|
||||
resize_result = self.resize_cylindrical_boss(face_id, target_diameter)
|
||||
else:
|
||||
resize_result = self.resize_cylindrical_hole(face_id, target_diameter)
|
||||
result_check = self._edge_length_result_summary_or_raise(plan)
|
||||
return (
|
||||
"Edge length resize completed by adjacent cylinder diameter edit: "
|
||||
f"edge {edge_id}, current_length={float(plan['current_length']):g}, "
|
||||
f"target_length={float(plan['target_length']):g}, "
|
||||
f"delta={float(plan['delta_length']):g}, "
|
||||
f"cylinder_face={face_id}, "
|
||||
f"target_diameter={target_diameter:g}, "
|
||||
f"mode={plan.get('circular_edge_cylinder_mode_label')}, "
|
||||
f"risk={plan['risk']}. {result_check} {resize_result}"
|
||||
)
|
||||
except Exception:
|
||||
if part is not None and old_part_shape is not None:
|
||||
part.shape = old_part_shape
|
||||
self.refresh_topology()
|
||||
raise
|
||||
|
||||
try:
|
||||
self._apply_edge_length_affine_transform(plan)
|
||||
result_check = self._edge_length_result_summary_or_raise(plan)
|
||||
return (
|
||||
"Edge length resize completed by adjacent cylinder diameter edit: "
|
||||
"Edge length resize completed by geometric scale fallback: "
|
||||
f"edge {edge_id}, current_length={float(plan['current_length']):g}, "
|
||||
f"target_length={float(plan['target_length']):g}, "
|
||||
f"delta={float(plan['delta_length']):g}, "
|
||||
f"cylinder_face={face_id}, "
|
||||
f"target_diameter={target_diameter:g}, "
|
||||
f"mode={plan.get('circular_edge_cylinder_mode_label')}, "
|
||||
f"risk={plan['risk']}. {result_check} {resize_result}"
|
||||
f"scale={float(plan['affine_scale']):g}, "
|
||||
f"transform={plan.get('affine_transform_label') or plan.get('affine_transform_kind')}, "
|
||||
f"predicted_edge_length={float(plan.get('affine_predicted_edge_length') or 0.0):g}, "
|
||||
f"axis_source={plan.get('affine_axis_source')}, "
|
||||
f"anchor={plan.get('edge_length_anchor_label')}, "
|
||||
f"target={plan.get('affine_target_kind')}, "
|
||||
f"risk={plan['risk']}. {result_check}"
|
||||
)
|
||||
|
||||
self._apply_edge_length_affine_transform(plan)
|
||||
result_check = self._edge_length_result_summary(plan)
|
||||
return (
|
||||
"Edge length resize completed by geometric scale fallback: "
|
||||
f"edge {edge_id}, current_length={float(plan['current_length']):g}, "
|
||||
f"target_length={float(plan['target_length']):g}, "
|
||||
f"delta={float(plan['delta_length']):g}, "
|
||||
f"scale={float(plan['affine_scale']):g}, "
|
||||
f"transform={plan.get('affine_transform_label') or plan.get('affine_transform_kind')}, "
|
||||
f"predicted_edge_length={float(plan.get('affine_predicted_edge_length') or 0.0):g}, "
|
||||
f"axis_source={plan.get('affine_axis_source')}, "
|
||||
f"anchor={plan.get('edge_length_anchor_label')}, "
|
||||
f"target={plan.get('affine_target_kind')}, "
|
||||
f"risk={plan['risk']}. {result_check}"
|
||||
)
|
||||
except Exception:
|
||||
if part is not None and old_part_shape is not None:
|
||||
part.shape = old_part_shape
|
||||
self.refresh_topology()
|
||||
raise
|
||||
|
||||
def move_edge_endpoint(
|
||||
self,
|
||||
@@ -6836,22 +6897,31 @@ class OperationMixin:
|
||||
if plan.get("resize_strategy") != "local-edge-endpoint-deform":
|
||||
raise ValueError("Unsupported Edge endpoint move strategy.")
|
||||
|
||||
self._apply_local_edge_deform(plan)
|
||||
result_check = self._edge_length_result_summary(plan)
|
||||
return (
|
||||
"Edge endpoint move completed by local edge deformation: "
|
||||
f"edge {edge_id}, "
|
||||
f"endpoint={plan.get('edge_endpoint_role')}, "
|
||||
f"current_endpoint={plan.get('current_endpoint_point')}, "
|
||||
f"target_endpoint={plan.get('target_endpoint_point')}, "
|
||||
f"move={plan.get('moved_endpoint_delta')}, "
|
||||
f"current_length={float(plan['current_length']):g}, "
|
||||
f"target_length={float(plan['target_length']):g}, "
|
||||
f"delta={float(plan['delta_length']):g}, "
|
||||
f"rebuilt_faces={plan.get('local_edge_deform_face_count')}, "
|
||||
f"target={plan.get('local_edge_deform_target_kind')}, "
|
||||
f"risk={plan['risk']}. {result_check}"
|
||||
)
|
||||
part_id = int(plan.get("part_id", -1))
|
||||
part = self.part_by_id(part_id) if part_id >= 0 else None
|
||||
old_part_shape = part.shape if part is not None else None
|
||||
try:
|
||||
self._apply_local_edge_deform(plan)
|
||||
result_check = self._edge_length_result_summary_or_raise(plan)
|
||||
return (
|
||||
"Edge endpoint move completed by local edge deformation: "
|
||||
f"edge {edge_id}, "
|
||||
f"endpoint={plan.get('edge_endpoint_role')}, "
|
||||
f"current_endpoint={plan.get('current_endpoint_point')}, "
|
||||
f"target_endpoint={plan.get('target_endpoint_point')}, "
|
||||
f"move={plan.get('moved_endpoint_delta')}, "
|
||||
f"current_length={float(plan['current_length']):g}, "
|
||||
f"target_length={float(plan['target_length']):g}, "
|
||||
f"delta={float(plan['delta_length']):g}, "
|
||||
f"rebuilt_faces={plan.get('local_edge_deform_face_count')}, "
|
||||
f"target={plan.get('local_edge_deform_target_kind')}, "
|
||||
f"risk={plan['risk']}. {result_check}"
|
||||
)
|
||||
except Exception:
|
||||
if part is not None and old_part_shape is not None:
|
||||
part.shape = old_part_shape
|
||||
self.refresh_topology()
|
||||
raise
|
||||
|
||||
def move_edge_center(
|
||||
self,
|
||||
@@ -6864,19 +6934,28 @@ class OperationMixin:
|
||||
if plan.get("resize_strategy") != "local-edge-center-deform":
|
||||
raise ValueError("Unsupported Edge center move strategy.")
|
||||
|
||||
self._apply_local_edge_deform(plan)
|
||||
result_check = self._edge_length_result_summary(plan)
|
||||
return (
|
||||
"Edge center move completed by local edge deformation: "
|
||||
f"edge {edge_id}, "
|
||||
f"current_center={plan.get('current_edge_center')}, "
|
||||
f"target_center={plan.get('target_edge_center')}, "
|
||||
f"move={plan.get('moved_edge_center_delta')}, "
|
||||
f"edge_length={float(plan['current_length']):g}, "
|
||||
f"rebuilt_faces={plan.get('local_edge_deform_face_count')}, "
|
||||
f"target={plan.get('local_edge_deform_target_kind')}, "
|
||||
f"risk={plan['risk']}. {result_check}"
|
||||
)
|
||||
part_id = int(plan.get("part_id", -1))
|
||||
part = self.part_by_id(part_id) if part_id >= 0 else None
|
||||
old_part_shape = part.shape if part is not None else None
|
||||
try:
|
||||
self._apply_local_edge_deform(plan)
|
||||
result_check = self._edge_length_result_summary_or_raise(plan)
|
||||
return (
|
||||
"Edge center move completed by local edge deformation: "
|
||||
f"edge {edge_id}, "
|
||||
f"current_center={plan.get('current_edge_center')}, "
|
||||
f"target_center={plan.get('target_edge_center')}, "
|
||||
f"move={plan.get('moved_edge_center_delta')}, "
|
||||
f"edge_length={float(plan['current_length']):g}, "
|
||||
f"rebuilt_faces={plan.get('local_edge_deform_face_count')}, "
|
||||
f"target={plan.get('local_edge_deform_target_kind')}, "
|
||||
f"risk={plan['risk']}. {result_check}"
|
||||
)
|
||||
except Exception:
|
||||
if part is not None and old_part_shape is not None:
|
||||
part.shape = old_part_shape
|
||||
self.refresh_topology()
|
||||
raise
|
||||
|
||||
def face_center_local_move_preview_polydata(
|
||||
self,
|
||||
@@ -7255,12 +7334,29 @@ class OperationMixin:
|
||||
action: Callable[[], object],
|
||||
) -> tuple[object, str]:
|
||||
snapshot = self._face_edit_snapshot(plan)
|
||||
previous_logical_ids = tuple(getattr(self, "face_logical_ids", ()))
|
||||
previous_faces = tuple(self.faces)
|
||||
target_logical_id = _int_or_none(plan.get("target_logical_id"))
|
||||
if target_logical_id is None:
|
||||
plan_face_id = _int_or_none(plan.get("face_id"))
|
||||
if plan_face_id is not None and 0 <= plan_face_id < len(self.faces):
|
||||
try:
|
||||
target_logical_id = self.face_region_logical_id(plan_face_id)
|
||||
except Exception:
|
||||
target_logical_id = None
|
||||
excluded_logical_ids = (target_logical_id,)
|
||||
try:
|
||||
action_result = action()
|
||||
self._apply_preserved_face_logical_ids_by_shape_identity(
|
||||
previous_faces,
|
||||
previous_logical_ids,
|
||||
excluded_logical_ids=excluded_logical_ids,
|
||||
)
|
||||
result_check = self._checked_face_edit_result_summary(plan)
|
||||
return action_result, result_check
|
||||
except Exception as exc:
|
||||
self._restore_face_edit_snapshot(snapshot)
|
||||
self._restore_face_logical_ids_if_count_matches(previous_logical_ids)
|
||||
raise RuntimeError(f"Face edit failed and the model was restored: {exc}") from exc
|
||||
|
||||
def _face_edit_result_summary(
|
||||
@@ -7579,6 +7675,41 @@ class OperationMixin:
|
||||
f"scope={check['scope']}."
|
||||
)
|
||||
|
||||
def _edge_length_result_summary_or_raise(self, plan: dict[str, object]) -> str:
|
||||
check = self._edge_length_result_check(plan)
|
||||
if check is None:
|
||||
raise RuntimeError("Edge长度编辑结果无法校验,模型已恢复到修改前状态。")
|
||||
target_length = float(plan.get("target_length", 0.0) or 0.0)
|
||||
length_tolerance = max(_shape_diagonal(self.shape) * 1e-5, target_length * 1e-4, 1e-5)
|
||||
if float(check.get("target_error", 0.0) or 0.0) > length_tolerance:
|
||||
raise RuntimeError(
|
||||
"Edge长度编辑结果没有达到目标值,模型已恢复到修改前状态。"
|
||||
f"最近Edge {check.get('edge_id')} 的长度约 {float(check.get('nearest_length', 0.0) or 0.0):g},"
|
||||
f"目标长度 {target_length:g},误差 {float(check.get('target_error', 0.0) or 0.0):g}。"
|
||||
)
|
||||
|
||||
expected = self._edge_length_expected_endpoints(plan)
|
||||
endpoint_error = float(check.get("endpoint_error", -1.0) or -1.0)
|
||||
if expected is not None and endpoint_error >= 0.0:
|
||||
endpoint_tolerance = max(_shape_diagonal(self.shape) * 1e-4, target_length * 1e-3, 1e-4)
|
||||
if endpoint_error > endpoint_tolerance:
|
||||
raise RuntimeError(
|
||||
"Edge长度编辑后的目标端点位置不符合所选建模意图,模型已恢复到修改前状态。"
|
||||
f"最近Edge {check.get('edge_id')} 的端点误差约 {endpoint_error:g},"
|
||||
f"允许误差 {endpoint_tolerance:g}。"
|
||||
)
|
||||
|
||||
return (
|
||||
"Result check: "
|
||||
f"match={check['match_method']}, "
|
||||
f"nearest_edge={check['edge_id']}, "
|
||||
f"nearest_length={float(check['nearest_length']):g}, "
|
||||
f"target_error={float(check['target_error']):g}, "
|
||||
f"relative_error={float(check['relative_error']):g}, "
|
||||
f"endpoint_error={float(check['endpoint_error']):g}, "
|
||||
f"scope={check['scope']}."
|
||||
)
|
||||
|
||||
def _edge_length_result_check(self, plan: dict[str, object]) -> dict[str, object] | None:
|
||||
try:
|
||||
target_length = float(plan.get("target_length", 0.0))
|
||||
@@ -7788,6 +7919,8 @@ class OperationMixin:
|
||||
points = self._orient_local_polygon_outward(points, center)
|
||||
if self._local_points_are_planar(points, tolerance):
|
||||
moved_faces.append(self._make_local_polygon_face(points))
|
||||
elif len(points) == 4:
|
||||
moved_faces.append(self._make_local_bilinear_quad_face(points, tolerance))
|
||||
else:
|
||||
for index in range(1, len(points) - 1):
|
||||
triangle = [points[0], points[index], points[index + 1]]
|
||||
@@ -8042,6 +8175,89 @@ class OperationMixin:
|
||||
if region_ids:
|
||||
self.assign_logical_face_region_exclusive(int(logical_id), region_ids)
|
||||
|
||||
def _apply_preserved_face_logical_ids_by_shape_identity(
|
||||
self,
|
||||
previous_faces: Iterable[TopoDS_Shape],
|
||||
previous_logical_ids: Iterable[int],
|
||||
*,
|
||||
excluded_logical_ids: Iterable[int | None] = (),
|
||||
) -> int:
|
||||
excluded = {int(item) for item in excluded_logical_ids if item is not None}
|
||||
old_faces = list(previous_faces)
|
||||
old_logical_ids = [int(item) for item in previous_logical_ids]
|
||||
if not old_faces or len(old_faces) != len(old_logical_ids):
|
||||
return 0
|
||||
|
||||
unmatched_new_ids: set[int] = set(range(len(self.faces)))
|
||||
pending: list[tuple[TopoDS_Shape, int]] = []
|
||||
matched_by_logical_id: dict[int, set[int]] = {}
|
||||
|
||||
for old_face_id, old_face in enumerate(old_faces):
|
||||
logical_id = old_logical_ids[old_face_id]
|
||||
if logical_id in excluded:
|
||||
continue
|
||||
if (
|
||||
old_face_id < len(self.faces)
|
||||
and old_face_id in unmatched_new_ids
|
||||
and _same_shape(old_face, self.faces[old_face_id])
|
||||
):
|
||||
matched_by_logical_id.setdefault(logical_id, set()).add(old_face_id)
|
||||
unmatched_new_ids.remove(old_face_id)
|
||||
else:
|
||||
pending.append((old_face, logical_id))
|
||||
|
||||
for old_face, logical_id in pending:
|
||||
if logical_id in excluded:
|
||||
continue
|
||||
match_id: int | None = None
|
||||
for new_face_id in tuple(unmatched_new_ids):
|
||||
if _same_shape(old_face, self.faces[new_face_id]):
|
||||
match_id = new_face_id
|
||||
break
|
||||
if match_id is None:
|
||||
continue
|
||||
matched_by_logical_id.setdefault(logical_id, set()).add(match_id)
|
||||
unmatched_new_ids.remove(match_id)
|
||||
|
||||
reassigned = 0
|
||||
assigned_face_ids: set[int] = set()
|
||||
logical_targets: dict[int, set[int]] = {}
|
||||
for logical_id, face_ids in sorted(matched_by_logical_id.items()):
|
||||
valid_face_ids = set(face_ids) - assigned_face_ids
|
||||
if not valid_face_ids:
|
||||
continue
|
||||
logical_targets[int(logical_id)] = valid_face_ids
|
||||
assigned_face_ids.update(valid_face_ids)
|
||||
reassigned += len(valid_face_ids)
|
||||
if not logical_targets:
|
||||
return 0
|
||||
|
||||
new_logical_ids = list(self.face_logical_ids)
|
||||
replacement_id = max(
|
||||
[len(self.faces), *[int(item) for item in new_logical_ids], *logical_targets.keys()],
|
||||
default=len(self.faces),
|
||||
) + 1
|
||||
for face_id, logical_id in enumerate(list(new_logical_ids)):
|
||||
target_face_ids = logical_targets.get(int(logical_id))
|
||||
if target_face_ids is None or face_id in target_face_ids:
|
||||
continue
|
||||
new_logical_ids[face_id] = replacement_id
|
||||
replacement_id += 1
|
||||
for logical_id, face_ids in logical_targets.items():
|
||||
for face_id in face_ids:
|
||||
if 0 <= face_id < len(new_logical_ids):
|
||||
new_logical_ids[face_id] = logical_id
|
||||
self.face_logical_ids = new_logical_ids
|
||||
self._quick_face_info_cache.clear()
|
||||
self._face_info_cache.clear()
|
||||
self._feature_info_cache.clear()
|
||||
self._same_domain_face_ids_cache.clear()
|
||||
self._face_first_level_topology_cache.clear()
|
||||
self._cylindrical_first_level_topology_cache.clear()
|
||||
self._face_first_level_fact_cache.clear()
|
||||
self._local_face_deform_readiness_cache.clear()
|
||||
return reassigned
|
||||
|
||||
def _local_face_deform_target(
|
||||
self,
|
||||
plan: dict[str, object],
|
||||
@@ -8177,6 +8393,27 @@ class OperationMixin:
|
||||
raise RuntimeError("Local edge deformation created an empty face.")
|
||||
return face
|
||||
|
||||
def _make_local_bilinear_quad_face(
|
||||
self,
|
||||
points: list[tuple[float, float, float]],
|
||||
tolerance: float,
|
||||
) -> TopoDS_Shape:
|
||||
if len(points) != 4:
|
||||
raise RuntimeError("Local edge deformation bilinear face needs exactly four points.")
|
||||
grid = TColgp_Array2OfPnt(1, 2, 1, 2)
|
||||
grid.SetValue(1, 1, gp_Pnt(*points[0]))
|
||||
grid.SetValue(2, 1, gp_Pnt(*points[1]))
|
||||
grid.SetValue(1, 2, gp_Pnt(*points[3]))
|
||||
grid.SetValue(2, 2, gp_Pnt(*points[2]))
|
||||
surface = GeomAPI_PointsToBSplineSurface(grid).Surface()
|
||||
maker = BRepBuilderAPI_MakeFace(surface, max(float(tolerance), 1e-7))
|
||||
if hasattr(maker, "IsDone") and not maker.IsDone():
|
||||
raise RuntimeError("Local edge deformation could not create a bilinear quad face.")
|
||||
face = maker.Face()
|
||||
if face.IsNull():
|
||||
raise RuntimeError("Local edge deformation created an empty bilinear quad face.")
|
||||
return face
|
||||
|
||||
def _dedupe_local_points(
|
||||
self,
|
||||
points: list[tuple[float, float, float]],
|
||||
|
||||
@@ -59,18 +59,36 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
|
||||
"first_level_fact_scope",
|
||||
"first_level_fact_source_model",
|
||||
"first_level_fact_subject_role",
|
||||
"first_level_fact_subject_edge_ids",
|
||||
"first_level_fact_subject_edge_count",
|
||||
"first_level_fact_subject_face_ids",
|
||||
"first_level_fact_subject_face_count",
|
||||
"first_level_fact_boundary_edge_ids",
|
||||
"first_level_fact_boundary_edge_count",
|
||||
"first_level_fact_boundary_vertex_points",
|
||||
"first_level_fact_boundary_vertex_count",
|
||||
"first_level_fact_adjacent_edge_ids",
|
||||
"first_level_fact_adjacent_edge_count",
|
||||
"first_level_fact_adjacent_face_ids",
|
||||
"first_level_fact_adjacent_face_count",
|
||||
"first_level_fact_included_edge_ids",
|
||||
"first_level_fact_included_edge_count",
|
||||
"first_level_fact_included_face_ids",
|
||||
"first_level_fact_included_face_count",
|
||||
"first_level_fact_role_groups",
|
||||
"first_level_fact_ignored_relation_depths",
|
||||
"first_level_fact_ignored_relation_note",
|
||||
"first_level_fact_summary",
|
||||
"selected_edge_ids",
|
||||
"selected_edge_count",
|
||||
"first_level_vertex_points",
|
||||
"first_level_vertex_count",
|
||||
"first_level_adjacent_edge_ids",
|
||||
"first_level_adjacent_edge_count",
|
||||
"first_level_edge_ids",
|
||||
"first_level_edge_count",
|
||||
"first_level_adjacent_face_boundary_edge_ids",
|
||||
"first_level_adjacent_face_boundary_edge_count",
|
||||
],
|
||||
),
|
||||
(
|
||||
@@ -408,6 +426,8 @@ INFO_LABELS = {
|
||||
"parent_id": "父级 ID",
|
||||
"depth": "层级深度",
|
||||
"feature_mode": "特征模式说明",
|
||||
"cad_modeling_form": "建模形式",
|
||||
"cad_recommended_operation": "推荐操作",
|
||||
"feature_type": "特征类型",
|
||||
"feature_source_face_id": "特征来源 Face",
|
||||
"recognition_summary": "识别摘要",
|
||||
@@ -446,18 +466,36 @@ INFO_LABELS = {
|
||||
"first_level_fact_scope": "一级事实范围",
|
||||
"first_level_fact_source_model": "一级事实来源",
|
||||
"first_level_fact_subject_role": "一级事实主体",
|
||||
"first_level_fact_subject_edge_ids": "一级主体 Edge",
|
||||
"first_level_fact_subject_edge_count": "一级主体 Edge 数",
|
||||
"first_level_fact_subject_face_ids": "一级主体 Face",
|
||||
"first_level_fact_subject_face_count": "一级主体 Face 数",
|
||||
"first_level_fact_boundary_edge_ids": "一级边界 Edge",
|
||||
"first_level_fact_boundary_edge_count": "一级边界 Edge 数",
|
||||
"first_level_fact_boundary_vertex_points": "一级边界 Vertex",
|
||||
"first_level_fact_boundary_vertex_count": "一级边界 Vertex 数",
|
||||
"first_level_fact_adjacent_edge_ids": "一级相邻 Edge",
|
||||
"first_level_fact_adjacent_edge_count": "一级相邻 Edge 数",
|
||||
"first_level_fact_adjacent_face_ids": "一级相邻 Face",
|
||||
"first_level_fact_adjacent_face_count": "一级相邻 Face 数",
|
||||
"first_level_fact_included_edge_ids": "一级范围 Edge",
|
||||
"first_level_fact_included_edge_count": "一级范围 Edge 数",
|
||||
"first_level_fact_included_face_ids": "一级范围 Face",
|
||||
"first_level_fact_included_face_count": "一级范围 Face 数",
|
||||
"first_level_fact_role_groups": "一级角色分组",
|
||||
"first_level_fact_ignored_relation_depths": "暂不传播层级",
|
||||
"first_level_fact_ignored_relation_note": "暂不传播说明",
|
||||
"first_level_fact_summary": "一级事实摘要",
|
||||
"selected_edge_ids": "当前 Edge",
|
||||
"selected_edge_count": "当前 Edge 数",
|
||||
"first_level_vertex_points": "一级端点 Vertex",
|
||||
"first_level_vertex_count": "一级端点 Vertex 数",
|
||||
"first_level_adjacent_edge_ids": "一级相邻 Edge",
|
||||
"first_level_adjacent_edge_count": "一级相邻 Edge 数",
|
||||
"first_level_edge_ids": "一级范围 Edge",
|
||||
"first_level_edge_count": "一级范围 Edge 数",
|
||||
"first_level_adjacent_face_boundary_edge_ids": "相邻 Face 的边界 Edge",
|
||||
"first_level_adjacent_face_boundary_edge_count": "相邻 Face 的边界 Edge 数",
|
||||
"volume": "体积",
|
||||
"surface_area": "表面积",
|
||||
"area": "面积",
|
||||
@@ -710,9 +748,9 @@ INFO_LABELS = {
|
||||
"freeform_face_risk": "自由曲面风险",
|
||||
"freeform_face_blockers": "自由曲面限制",
|
||||
"resize_status": "切削状态",
|
||||
"resize_strategy": "编辑策略",
|
||||
"edit_strategy_label": "编辑策略说明",
|
||||
"edit_semantics": "编辑方式",
|
||||
"resize_strategy": "执行策略",
|
||||
"edit_strategy_label": "执行策略说明",
|
||||
"edit_semantics": "建模意图",
|
||||
"resize_mode": "调整模式",
|
||||
"resize_risk": "切削风险",
|
||||
"resize_warnings": "切削警告",
|
||||
|
||||
@@ -375,8 +375,8 @@ class WindowActionMixin:
|
||||
f"本次移动距离: {_format_value(distance)}\n"
|
||||
f"当前法向位置: {_format_value(plan.get('current_plane_position'))}\n"
|
||||
f"目标法向位置: {_format_value(plan.get('target_plane_position'))}\n"
|
||||
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"影响范围: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"执行影响: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
@@ -1153,7 +1153,7 @@ class WindowActionMixin:
|
||||
face_id,
|
||||
blocker,
|
||||
resize_strategy="local-face-plane-offset-deform",
|
||||
edit_strategy_label="偏移曲面(局部重建)",
|
||||
edit_strategy_label="偏移变换(局部重建)",
|
||||
edit_semantics="按当前面垂直方向移动 Face 边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
|
||||
info=info,
|
||||
extra={
|
||||
@@ -1394,9 +1394,9 @@ class WindowActionMixin:
|
||||
strategy_label = str(plan.get("edit_strategy_label") or "").strip()
|
||||
edit_semantics = str(plan.get("edit_semantics") or "").strip()
|
||||
if strategy_label:
|
||||
semantic_lines.append(f"编辑策略: {strategy_label}")
|
||||
semantic_lines.append(f"执行策略: {strategy_label}")
|
||||
if edit_semantics:
|
||||
semantic_lines.append(f"编辑方式: {edit_semantics}")
|
||||
semantic_lines.append(f"建模意图: {edit_semantics}")
|
||||
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
@@ -1432,7 +1432,7 @@ class WindowActionMixin:
|
||||
"V向尺寸(局部重建)",
|
||||
"U向尺寸(缩放特征)",
|
||||
"V向尺寸(缩放特征)",
|
||||
"偏移曲面(局部重建)",
|
||||
"偏移变换(局部重建)",
|
||||
"中心(局部重建)",
|
||||
"圆柱高度调整",
|
||||
"高度(缩放特征)缩放所属对象",
|
||||
@@ -1511,7 +1511,7 @@ class WindowActionMixin:
|
||||
|
||||
strategy = str(parameters.get("edit_strategy_label") or "").strip()
|
||||
if strategy:
|
||||
lines.append(f"编辑策略: {strategy}")
|
||||
lines.append(f"执行策略: {strategy}")
|
||||
|
||||
surface = parameters.get("surface")
|
||||
distance = parameters.get("semantic_distance")
|
||||
@@ -2844,8 +2844,8 @@ class WindowActionMixin:
|
||||
f"置信度: {plan['confidence']}\n"
|
||||
f"材料投票: {plan['material_vote_summary']}\n"
|
||||
f"工具策略: {plan['boss_tool_strategy']}\n"
|
||||
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"建模意图: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"工具高度: {_format_value(plan['boss_tool_height'])}\n"
|
||||
f"轴向重叠余量: {_format_value(plan['boss_tool_axial_margin'])}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
@@ -3034,8 +3034,8 @@ class WindowActionMixin:
|
||||
f"端盖拉伸/切除距离: {_format_value(plan.get('push_pull_distance'))}\n"
|
||||
f"候选判断: {plan.get('feature_guess')}\n"
|
||||
f"置信度: {plan.get('confidence')}\n"
|
||||
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"建模意图: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
@@ -3737,8 +3737,8 @@ class WindowActionMixin:
|
||||
f"支撑 Face: {_format_value(plan.get('feature_existing_fillet_support_face_ids', ''))}\n"
|
||||
f"边界 Edge: {_format_value(plan.get('feature_boundary_edge_ids', ''))}\n"
|
||||
f"策略: {plan['resize_strategy']}\n"
|
||||
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"建模意图: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
@@ -3828,8 +3828,8 @@ class WindowActionMixin:
|
||||
f"半径/Edge长度比例: {_format_percent(plan['radius_to_length_ratio'])}\n"
|
||||
f"相邻 Face: {_format_value(plan.get('adjacent_face_ids', ''))}\n"
|
||||
f"相邻 Face 数: {_format_value(plan.get('adjacent_face_count', ''))}\n"
|
||||
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"建模意图: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
@@ -3910,8 +3910,8 @@ class WindowActionMixin:
|
||||
f"倒角距离/Edge长度比例: {_format_percent(plan['distance_to_length_ratio'])}\n"
|
||||
f"相邻 Face: {_format_value(plan.get('adjacent_face_ids', ''))}\n"
|
||||
f"相邻 Face 数: {_format_value(plan.get('adjacent_face_count', ''))}\n"
|
||||
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"建模意图: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
@@ -4175,9 +4175,9 @@ class WindowActionMixin:
|
||||
f"目标Edge长度: {_format_value(plan['target_length'])}\n"
|
||||
f"变化量: {_format_value(plan['delta_length'])}\n"
|
||||
f"Edge长度基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n"
|
||||
f"Edge修改策略: {_format_value(plan.get('edge_length_strategy_label', ''))}\n"
|
||||
f"建模意图: {_format_value(plan.get('edge_length_strategy_label', ''))}\n"
|
||||
f"固定约束: {_format_value(plan.get('edge_length_constraint_summary', ''))}\n"
|
||||
f"影响范围: {_format_value(plan.get('edge_length_impact_summary', ''))}\n"
|
||||
f"执行影响: {_format_value(plan.get('edge_length_impact_summary', ''))}\n"
|
||||
f"策略: {_format_value(plan.get('resize_strategy', ''))}\n"
|
||||
f"局部形变目标: {_format_value(plan.get('local_edge_deform_target_kind', ''))}\n"
|
||||
f"局部形变: {_format_value(plan.get('local_edge_deform_anchor', ''))}\n"
|
||||
@@ -4343,9 +4343,9 @@ class WindowActionMixin:
|
||||
f"目标Edge长度: {_format_value(plan['target_length'])}\n"
|
||||
f"变化量: {_format_value(plan['delta_length'])}\n"
|
||||
f"Edge长度基准: {_format_value(plan.get('edge_length_anchor_label', ''))}\n"
|
||||
f"Edge修改策略: {_format_value(plan.get('edge_length_strategy_label', ''))}\n"
|
||||
f"建模意图: {_format_value(plan.get('edge_length_strategy_label', ''))}\n"
|
||||
f"固定约束: {_format_value(plan.get('edge_length_constraint_summary', ''))}\n"
|
||||
f"影响范围: {_format_value(plan.get('edge_length_impact_summary', ''))}\n"
|
||||
f"执行影响: {_format_value(plan.get('edge_length_impact_summary', ''))}\n"
|
||||
f"策略: {_format_value(strategy)}\n"
|
||||
f"局部形变目标: {_format_value(plan.get('local_edge_deform_target_kind', ''))}\n"
|
||||
f"局部形变: {_format_value(plan.get('local_edge_deform_anchor', ''))}\n"
|
||||
@@ -4516,7 +4516,7 @@ class WindowActionMixin:
|
||||
f"目标小半径: {_format_value(plan.get('ellipse_target_minor_radius'))}\n"
|
||||
f"缩放方向: {_format_value(plan.get('ellipse_axis_direction_label'))}\n"
|
||||
f"缩放比例: {_format_value(plan.get('affine_scale'))}\n"
|
||||
f"影响范围: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"执行影响: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
@@ -4883,8 +4883,8 @@ class WindowActionMixin:
|
||||
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n"
|
||||
f"缩放比例: {_format_value(plan.get('affine_scale'))}\n"
|
||||
f"策略: {_format_value(plan.get('resize_strategy'))}\n"
|
||||
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"建模意图: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
@@ -4980,8 +4980,8 @@ class WindowActionMixin:
|
||||
f"目标半角: {_format_value(plan.get('target_semi_angle_degrees'))} 度\n"
|
||||
f"目标参考半径: {_format_value(plan.get('target_reference_radius'))}\n"
|
||||
f"变化比例: {_format_percent(plan.get('reference_radius_delta_ratio'))}\n"
|
||||
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"影响范围: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"执行影响: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
@@ -5079,8 +5079,8 @@ class WindowActionMixin:
|
||||
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n"
|
||||
f"缩放比例: {_format_value(plan.get('affine_scale'))}\n"
|
||||
f"策略: {_format_value(plan.get('resize_strategy'))}\n"
|
||||
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"建模意图: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
@@ -5184,8 +5184,8 @@ class WindowActionMixin:
|
||||
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n"
|
||||
f"缩放比例: {_format_value(plan.get('affine_scale'))}\n"
|
||||
f"策略: {_format_value(plan.get('resize_strategy'))}\n"
|
||||
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"建模意图: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
@@ -5277,8 +5277,8 @@ class WindowActionMixin:
|
||||
f"缩放目标: {_format_value(plan.get('affine_target_kind'))}\n"
|
||||
f"缩放比例: {_format_value(plan.get('affine_scale'))}\n"
|
||||
f"策略: {_format_value(plan.get('resize_strategy'))}\n"
|
||||
f"编辑策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"编辑方式: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"执行策略: {_format_value(plan.get('edit_strategy_label'))}\n"
|
||||
f"建模意图: {_format_value(plan.get('edit_semantics'))}\n"
|
||||
f"影响说明: {_format_value(plan.get('affine_transform_note'))}\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
@@ -5625,7 +5625,7 @@ class WindowActionMixin:
|
||||
def move_selected_face_plane_position_by_translation(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
if self._edit_busy("请等待当前编辑完成后再按偏移曲面平移所属对象。"):
|
||||
if self._edit_busy("请等待当前编辑完成后再按偏移变换平移所属对象。"):
|
||||
return
|
||||
if self.selected_face_id is None:
|
||||
QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。")
|
||||
@@ -5633,14 +5633,14 @@ class WindowActionMixin:
|
||||
try:
|
||||
distance = float(self.offset_input.text())
|
||||
except (AttributeError, ValueError):
|
||||
QMessageBox.critical(self, "偏移曲面无效", "请输入数字形式的目标偏移曲面。")
|
||||
QMessageBox.critical(self, "偏移变换无效", "请输入数字形式的目标偏移变换位置。")
|
||||
return
|
||||
face_id = self.selected_face_id
|
||||
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("偏移曲面整体平移已阻止")
|
||||
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)
|
||||
@@ -5652,7 +5652,7 @@ class WindowActionMixin:
|
||||
target_position,
|
||||
plane_direction,
|
||||
):
|
||||
self.statusBar().showMessage("已取消偏移曲面整体平移")
|
||||
self.statusBar().showMessage("已取消偏移变换整体平移")
|
||||
return
|
||||
self.clear_edit_preview(render=False)
|
||||
|
||||
@@ -5663,7 +5663,7 @@ class WindowActionMixin:
|
||||
moved_id = plan.get("solid_id") if moved_kind == "solid" else plan.get("part_id")
|
||||
self._run_edit_action(
|
||||
action,
|
||||
operation_name="偏移曲面(移动特征)",
|
||||
operation_name="偏移变换(移动特征)",
|
||||
target=f"Face {face_id}",
|
||||
parameters={
|
||||
"face_id": face_id,
|
||||
@@ -5684,8 +5684,8 @@ class WindowActionMixin:
|
||||
"translate_warnings": plan.get("warnings"),
|
||||
"translate_blockers": plan.get("blockers"),
|
||||
"resize_strategy": "translate-owning-shape-from-plane-offset",
|
||||
"edit_strategy_label": "按偏移曲面平移所属对象",
|
||||
"edit_semantics": "把目标偏移曲面换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。",
|
||||
"edit_strategy_label": "按偏移变换平移所属对象",
|
||||
"edit_semantics": "把目标偏移变换位置换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。",
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
@@ -5694,7 +5694,7 @@ class WindowActionMixin:
|
||||
def move_selected_face_plane_position_local(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
if self._edit_busy("请等待当前编辑完成后再执行偏移曲面(局部重建)。"):
|
||||
if self._edit_busy("请等待当前编辑完成后再执行偏移变换(局部重建)。"):
|
||||
return
|
||||
if self.selected_face_id is None:
|
||||
QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。")
|
||||
@@ -5702,7 +5702,7 @@ class WindowActionMixin:
|
||||
try:
|
||||
distance = float(self.offset_input.text())
|
||||
except (AttributeError, ValueError):
|
||||
QMessageBox.critical(self, "偏移曲面无效", "请输入数字形式的目标偏移曲面。")
|
||||
QMessageBox.critical(self, "偏移变换无效", "请输入数字形式的目标偏移变换位置。")
|
||||
return
|
||||
|
||||
face_id = self.selected_face_id
|
||||
@@ -5711,8 +5711,8 @@ class WindowActionMixin:
|
||||
plan = self.model.face_plane_offset_local_plan(face_id, distance)
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
f"当前偏移曲面: {_format_value(plan.get('current_plane_position'))}",
|
||||
f"目标偏移曲面: {_format_value(plan.get('target_plane_position'))}",
|
||||
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'))}",
|
||||
@@ -5722,11 +5722,11 @@ class WindowActionMixin:
|
||||
f"影响目标: {_format_value(plan.get('local_face_deform_target_kind'))}",
|
||||
]
|
||||
if not self._confirm_quick_edit_plan(
|
||||
"偏移曲面(局部重建)",
|
||||
"偏移变换(局部重建)",
|
||||
plan,
|
||||
lines,
|
||||
"偏移曲面(局部重建)已阻止",
|
||||
"已取消偏移曲面(局部重建)",
|
||||
"偏移变换(局部重建)已阻止",
|
||||
"已取消偏移变换(局部重建)",
|
||||
):
|
||||
return
|
||||
self.clear_edit_preview(render=False)
|
||||
@@ -5736,7 +5736,7 @@ class WindowActionMixin:
|
||||
|
||||
self._run_edit_action(
|
||||
action,
|
||||
operation_name="偏移曲面(局部重建)",
|
||||
operation_name="偏移变换(局部重建)",
|
||||
target=f"Face {face_id}",
|
||||
parameters={
|
||||
"face_id": face_id,
|
||||
@@ -5758,9 +5758,9 @@ class WindowActionMixin:
|
||||
"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"),
|
||||
"resize_strategy": "local-face-plane-offset-deform",
|
||||
"edit_strategy_label": "偏移曲面(局部重建)",
|
||||
"edit_strategy_label": "偏移变换(局部重建)",
|
||||
"edit_semantics": (
|
||||
"把目标偏移曲面换算成沿当前面垂直方向的移动量,移动当前 Face 的顶点并重建相邻平面;"
|
||||
"把目标偏移变换位置换算成沿当前面垂直方向的移动量,移动当前 Face 的顶点并重建相邻平面;"
|
||||
"不拉伸/切除加料/切削,也不平移所属对象。"
|
||||
),
|
||||
"resize_status": plan.get("status"),
|
||||
@@ -6067,8 +6067,8 @@ class WindowActionMixin:
|
||||
f"目标轴心: {_format_value(target_center)}\n"
|
||||
f"平移向量: {_format_value(plan['translation_vector'])}\n"
|
||||
f"平移距离: {_format_value(plan['translation_distance'])}\n"
|
||||
f"编辑策略: 按轴心平移所属对象\n"
|
||||
f"编辑方式: 平移所属特征或 Solid;不填旧孔、不切新孔、不重建局部凸台。\n"
|
||||
f"执行策略: 按轴心平移所属对象\n"
|
||||
f"建模意图: 平移所属特征或 Solid;不填旧孔、不切新孔、不重建局部凸台。\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
@@ -6157,8 +6157,8 @@ class WindowActionMixin:
|
||||
f"目标Face中心: {_format_value(target_center)}\n"
|
||||
f"平移向量: {_format_value(plan['translation_vector'])}\n"
|
||||
f"平移距离: {_format_value(plan['translation_distance'])}\n"
|
||||
f"编辑策略: 平移Face所属对象\n"
|
||||
f"编辑方式: 把目标 Face 的中心坐标换算成平移量,并平移所属特征或 Solid;不做单面局部扭曲。\n"
|
||||
f"执行策略: 平移Face所属对象\n"
|
||||
f"建模意图: 把目标 Face 的中心坐标换算成平移量,并平移所属特征或 Solid;不做单面局部扭曲。\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
@@ -6184,23 +6184,23 @@ class WindowActionMixin:
|
||||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"确认偏移曲面整体平移",
|
||||
"确认偏移变换整体平移",
|
||||
(
|
||||
f"Face: {face_id}\n"
|
||||
f"移动对象: {_format_value(plan.get('target_kind', ''))}\n"
|
||||
f"零件: {_format_value(plan.get('part_id', ''))}\n"
|
||||
f"Solid: {_format_value(plan.get('solid_id', ''))}\n"
|
||||
f"当前偏移曲面: {_format_value(current_position)}\n"
|
||||
f"目标偏移曲面: {_format_value(target_position)}\n"
|
||||
f"当前偏移变换位置: {_format_value(current_position)}\n"
|
||||
f"目标偏移变换位置: {_format_value(target_position)}\n"
|
||||
f"平面方向: {_format_value(plane_direction)}\n"
|
||||
f"平移向量: {_format_value(plan['translation_vector'])}\n"
|
||||
f"平移距离: {_format_value(plan['translation_distance'])}\n"
|
||||
f"编辑策略: 按偏移曲面平移所属对象\n"
|
||||
f"编辑方式: 沿当前面垂直方向平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。\n"
|
||||
f"执行策略: 按偏移变换平移所属对象\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,
|
||||
@@ -7577,7 +7577,7 @@ class WindowActionMixin:
|
||||
if target_position is not None and plane_direction is not None:
|
||||
checks.append(
|
||||
(
|
||||
"偏移曲面",
|
||||
"偏移变换",
|
||||
self._face_target_plane_position_matches(
|
||||
model,
|
||||
face_ids,
|
||||
@@ -8007,7 +8007,7 @@ class WindowActionMixin:
|
||||
"中心",
|
||||
"U向尺寸",
|
||||
"V向尺寸",
|
||||
"偏移曲面",
|
||||
"偏移变换",
|
||||
"壳体",
|
||||
"拉伸/切除",
|
||||
"圆锥",
|
||||
@@ -8062,9 +8062,9 @@ class WindowActionMixin:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会沿选中 Face 的平面内方向缩放所属特征或 Solid,其它几何会跟随变化。"
|
||||
elif "U向尺寸(局部重建)" in operation_name or "V向尺寸(局部重建)" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会只沿选中 Face 的一个平面内方向缩放顶点并重建相邻平面。"
|
||||
elif "偏移曲面(局部重建)" in operation_name:
|
||||
elif "偏移变换(局部重建)" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会沿当前面垂直方向移动所选 Face 顶点并重建相邻平面。"
|
||||
elif "偏移曲面(移动特征)" in operation_name:
|
||||
elif "偏移变换(移动特征)" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会沿当前面垂直方向平移所属特征或 Solid,不拉伸/切除。"
|
||||
elif "Face中心" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会平移所属特征或 Solid,不做单面局部扭曲。"
|
||||
|
||||
@@ -1930,6 +1930,7 @@ class WindowCoreMixin:
|
||||
self.selected_edge_id = edge_id
|
||||
self.selected_pick_position = pick_position
|
||||
info = self.model.edge_info(edge_id)
|
||||
info.update(self._edge_first_level_selection_fields(edge_id))
|
||||
self._sync_edge_edit_inputs(info)
|
||||
self.selected_part_id = int(info["part_id"])
|
||||
self.selected_solid_id = int(info["solid_id"]) if int(info.get("solid_id", -1)) >= 0 else None
|
||||
|
||||
+332
-39
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
import math
|
||||
from pathlib import Path
|
||||
import time
|
||||
from PySide6.QtCore import Qt, QThread, QTimer, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -29,6 +30,8 @@ PROPERTY_TARGET_COLUMN = 3
|
||||
PROPERTY_ACTION_COLUMN = 4
|
||||
|
||||
FEATURE_EDIT_SEMANTICS_KEYS = {
|
||||
"cad_modeling_form",
|
||||
"cad_recommended_operation",
|
||||
"face_first_level_topology",
|
||||
"cylindrical_feature_first_level_topology",
|
||||
"slot_edit_semantics",
|
||||
@@ -338,6 +341,59 @@ class WindowStateMixin:
|
||||
**self._first_level_fact_selection_fields(face_id, scope="cylindrical-feature"),
|
||||
}
|
||||
|
||||
def _edge_first_level_selection_fields(self, edge_id: int) -> dict[str, object]:
|
||||
if self.model is None:
|
||||
return {}
|
||||
try:
|
||||
topology = self.model.edge_first_level_topology(edge_id)
|
||||
facts = self.model.edge_first_level_facts(edge_id)
|
||||
except Exception as exc:
|
||||
return {
|
||||
"topology_relation_depth": 1,
|
||||
"topology_relation_model": "STEP/B-Rep edge first-level",
|
||||
"topology_relation_status": "unavailable",
|
||||
"topology_relation_message": str(exc),
|
||||
"topology_ignored_relation_depths": ("second-level", "third-level", "deeper"),
|
||||
"topology_ignored_relation_note": "Current Edge first-level topology is unavailable.",
|
||||
"first_level_vertex_count": 0,
|
||||
"first_level_adjacent_edge_count": 0,
|
||||
"first_level_adjacent_face_count": 0,
|
||||
"first_level_fact_model": "STEP/B-Rep first-level fact graph",
|
||||
"first_level_fact_status": "unavailable",
|
||||
"first_level_fact_source_model": "edge",
|
||||
"first_level_fact_relation_depth": 1,
|
||||
"first_level_fact_scope": "edge",
|
||||
"first_level_fact_subject_edge_ids": (edge_id,),
|
||||
"first_level_fact_subject_edge_count": 1,
|
||||
"first_level_fact_boundary_edge_count": 1,
|
||||
"first_level_fact_boundary_vertex_count": 0,
|
||||
"first_level_fact_adjacent_edge_count": 0,
|
||||
"first_level_fact_adjacent_face_count": 0,
|
||||
"first_level_fact_ignored_relation_depths": ("second-level", "third-level", "deeper"),
|
||||
"first_level_fact_summary": f"Current Edge first-level fact graph is unavailable: {exc}",
|
||||
}
|
||||
return {
|
||||
"topology_relation_depth": topology.get("topology_relation_depth", 1),
|
||||
"topology_relation_model": topology.get("topology_relation_model"),
|
||||
"topology_relation_scope": topology.get("topology_relation_scope"),
|
||||
"topology_relation_boundary": topology.get("topology_relation_boundary"),
|
||||
"topology_relation_status": "ready",
|
||||
"topology_ignored_relation_depths": topology.get("topology_ignored_relation_depths", ()),
|
||||
"topology_ignored_relation_note": topology.get("topology_ignored_relation_note", ""),
|
||||
"selected_edge_ids": topology.get("selected_edge_ids", (edge_id,)),
|
||||
"selected_edge_count": topology.get("selected_edge_count", 1),
|
||||
"first_level_vertex_points": topology.get("first_level_vertex_points", ()),
|
||||
"first_level_vertex_count": topology.get("first_level_vertex_count", 0),
|
||||
"first_level_adjacent_edge_ids": topology.get("first_level_adjacent_edge_ids", ()),
|
||||
"first_level_adjacent_edge_count": topology.get("first_level_adjacent_edge_count", 0),
|
||||
"first_level_edge_ids": topology.get("first_level_edge_ids", (edge_id,)),
|
||||
"first_level_edge_count": topology.get("first_level_edge_count", 1),
|
||||
"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_topology_note": topology.get("first_level_topology_note", ""),
|
||||
**facts,
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -415,22 +471,28 @@ class WindowStateMixin:
|
||||
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 = self._feature_info_for_selected_face(face_id, self.model.quick_face_info(face_id))
|
||||
if str(root_info.get("surface", "") or "") == "plane" and "topology_relation_depth" not in root_info:
|
||||
root_info.update(self._face_first_level_selection_fields(face_id))
|
||||
elif str(root_info.get("surface", "") or "") == "cylinder" and detection_level != "current-only":
|
||||
root_info.update(self._cylindrical_first_level_selection_fields(face_id))
|
||||
associated: list[dict[str, object]] = []
|
||||
if detection_level in {"associated-only", "secondary"}:
|
||||
try:
|
||||
associated = self.model.associated_feature_infos(face_id)
|
||||
associated = self.model.associated_feature_infos(
|
||||
face_id,
|
||||
max_depth=3,
|
||||
max_scan_faces=72,
|
||||
max_features=10,
|
||||
time_budget_seconds=0.55 if detection_level == "associated-only" else 0.75,
|
||||
lightweight=True,
|
||||
)
|
||||
except Exception:
|
||||
associated = []
|
||||
if detection_level == "secondary":
|
||||
associated = self._secondary_associated_feature_infos(face_id, associated)
|
||||
associated = self._secondary_associated_feature_infos(
|
||||
face_id,
|
||||
associated,
|
||||
time_budget_seconds=0.45,
|
||||
)
|
||||
|
||||
highlight_ids = set(_int_values(root_info.get("feature_highlight_face_ids")) or [face_id])
|
||||
for item in associated:
|
||||
@@ -509,11 +571,19 @@ class WindowStateMixin:
|
||||
self,
|
||||
source_face_id: int,
|
||||
direct_infos: list[dict[str, object]],
|
||||
*,
|
||||
time_budget_seconds: float | None = None,
|
||||
) -> 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()
|
||||
deadline = None
|
||||
if time_budget_seconds is not None and time_budget_seconds > 0:
|
||||
deadline = time.monotonic() + float(time_budget_seconds)
|
||||
|
||||
def budget_expired() -> bool:
|
||||
return deadline is not None and time.monotonic() >= deadline
|
||||
|
||||
def add(info: dict[str, object]) -> None:
|
||||
source_id = _int_or_none(info.get("association_source_face_id"))
|
||||
@@ -529,17 +599,26 @@ class WindowStateMixin:
|
||||
for info in direct_infos:
|
||||
add(info)
|
||||
for info in list(results):
|
||||
if budget_expired():
|
||||
break
|
||||
parent_id = _int_or_none(info.get("association_source_face_id"))
|
||||
if parent_id is None:
|
||||
continue
|
||||
try:
|
||||
nested_budget = None
|
||||
if deadline is not None:
|
||||
nested_budget = max(0.02, min(0.12, deadline - time.monotonic()))
|
||||
for nested in self.model.associated_feature_infos(
|
||||
parent_id,
|
||||
max_depth=2,
|
||||
max_scan_faces=48,
|
||||
max_features=6,
|
||||
max_depth=1,
|
||||
max_scan_faces=16,
|
||||
max_features=4,
|
||||
time_budget_seconds=nested_budget,
|
||||
lightweight=True,
|
||||
):
|
||||
add(nested)
|
||||
if budget_expired():
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
results.sort(
|
||||
@@ -1293,7 +1372,7 @@ class WindowStateMixin:
|
||||
action_item = self._property_table_item(action_text, editable=False)
|
||||
target_item.setToolTip(self._property_target_tooltip(effective_spec, editable=editable))
|
||||
row_items = (label_item, current_item, scope_item, target_item, action_item)
|
||||
self._style_property_row_items(row_items, editable=editable)
|
||||
self._style_property_row_items(row_items, editable=editable, spec=effective_spec)
|
||||
for column, item in enumerate(row_items):
|
||||
item.setToolTip(item.toolTip() or item.text())
|
||||
self.property_table.setItem(row, column, item)
|
||||
@@ -1319,6 +1398,8 @@ class WindowStateMixin:
|
||||
|
||||
def _sort_property_specs_for_display(self, specs: list[dict[str, object]]) -> list[dict[str, object]]:
|
||||
def rank(spec: dict[str, object]) -> int:
|
||||
if bool(spec.get("pin_top")):
|
||||
return -1
|
||||
editable = bool(spec.get("editable"))
|
||||
enabled = bool(spec.get("enabled"))
|
||||
has_action = bool(spec.get("action"))
|
||||
@@ -1340,8 +1421,24 @@ class WindowStateMixin:
|
||||
items: tuple[QTableWidgetItem, QTableWidgetItem, QTableWidgetItem, QTableWidgetItem, QTableWidgetItem],
|
||||
*,
|
||||
editable: bool,
|
||||
spec: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
label_item, current_item, scope_item, target_item, status_item = items
|
||||
if spec is not None and bool(spec.get("pin_top")):
|
||||
for item in items:
|
||||
item.setBackground(QColor("#eef6ff"))
|
||||
label_item.setForeground(QColor("#075985"))
|
||||
current_item.setForeground(QColor("#0f172a"))
|
||||
scope_item.setForeground(QColor("#0369a1"))
|
||||
target_item.setForeground(QColor("#64748b"))
|
||||
status_item.setForeground(QColor("#0369a1"))
|
||||
label_font = label_item.font()
|
||||
label_font.setBold(True)
|
||||
label_item.setFont(label_font)
|
||||
current_font = current_item.font()
|
||||
current_font.setBold(True)
|
||||
current_item.setFont(current_font)
|
||||
return
|
||||
if editable:
|
||||
row_backgrounds = ("#fff7ed", "#fffbeb", "#fff7ed", "#fff7ed", "#fff7ed")
|
||||
for item, color in zip(items, row_backgrounds):
|
||||
@@ -1421,8 +1518,8 @@ class WindowStateMixin:
|
||||
tip_key = "enabled_tip" if bool(mode.get("enabled", True)) else "disabled_tip"
|
||||
tip = str(mode.get(tip_key) or mode.get("enabled_tip") or mode.get("disabled_tip") or "").strip()
|
||||
if tip:
|
||||
return f"影响范围:{label}\n\n{tip}"
|
||||
return f"影响范围:{label}"
|
||||
return f"建模意图:{label}\n\n{tip}"
|
||||
return f"建模意图:{label}"
|
||||
|
||||
def _set_property_target_editor(self, row: int, spec: dict[str, object]) -> None:
|
||||
editor = QLineEdit(str(spec.get("target_text", "")))
|
||||
@@ -1632,10 +1729,15 @@ class WindowStateMixin:
|
||||
dimension["label"] = label_overrides[key]
|
||||
dimensions.append(dimension)
|
||||
|
||||
pinned_explanations = [
|
||||
dict(spec)
|
||||
for spec in specs
|
||||
if str(spec.get("key", "")) in FEATURE_EDIT_SEMANTICS_KEYS and bool(spec.get("pin_top"))
|
||||
]
|
||||
explanations = [
|
||||
dict(spec)
|
||||
for spec in specs
|
||||
if str(spec.get("key", "")) in FEATURE_EDIT_SEMANTICS_KEYS
|
||||
if str(spec.get("key", "")) in FEATURE_EDIT_SEMANTICS_KEYS and not bool(spec.get("pin_top"))
|
||||
]
|
||||
if not dimensions:
|
||||
dimensions.append(
|
||||
@@ -1654,7 +1756,7 @@ class WindowStateMixin:
|
||||
),
|
||||
}
|
||||
)
|
||||
return dimensions + explanations
|
||||
return pinned_explanations + dimensions + explanations
|
||||
|
||||
def _feature_context_property_specs(
|
||||
self,
|
||||
@@ -1675,6 +1777,7 @@ class WindowStateMixin:
|
||||
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"))
|
||||
feature_label = str(related_info.get("association_label") or feature_label)
|
||||
for spec in self._feature_property_specs(related_specs, related_info):
|
||||
if spec.get("parameter_role") != "dimension":
|
||||
continue
|
||||
@@ -2149,6 +2252,7 @@ class WindowStateMixin:
|
||||
label: str,
|
||||
text: str,
|
||||
tip: str,
|
||||
pin_top: bool = False,
|
||||
) -> None:
|
||||
specs.append(
|
||||
{
|
||||
@@ -2161,9 +2265,196 @@ class WindowStateMixin:
|
||||
"enabled": False,
|
||||
"status_text": "说明",
|
||||
"disabled_tip": tip or text,
|
||||
"pin_top": pin_top,
|
||||
}
|
||||
)
|
||||
|
||||
def cad_modeling_form() -> tuple[str, str]:
|
||||
if has_face:
|
||||
if is_hole_or_groove:
|
||||
if is_slot_or_half_hole:
|
||||
return (
|
||||
"工程特征 / 槽:按槽宽、槽深、弧长、总长或轴心做局部重建。",
|
||||
"这类对象优先按 Creo 式槽特征理解;当前只传播直接相邻的一级关系,复杂二级/三级联动后续再开放。",
|
||||
)
|
||||
return (
|
||||
"工程特征 / 孔:按孔径、轴心、封堵或盲孔/盲槽深度做重切/补料。",
|
||||
"这类对象优先按 Creo 式孔特征理解;通孔、盲孔、锥孔和槽孔会根据识别结果开放不同参数。",
|
||||
)
|
||||
if is_boss:
|
||||
return (
|
||||
"工程特征 / 凸台:按直径、高度或轴心做局部重建。",
|
||||
"凸台修改会尽量按局部补料、切除端盖或重建包络处理;也可以选择缩放所属对象这类整体语义。",
|
||||
)
|
||||
if is_existing_fillet:
|
||||
return (
|
||||
"工程特征 / 倒圆角:移除已有圆角后按目标半径重新倒圆。",
|
||||
"STEP 没有原始倒圆角历史时,只能基于当前圆角面和支撑面推断;复杂 blend 会阻止或回滚。",
|
||||
)
|
||||
if is_shell_candidate:
|
||||
return (
|
||||
"工程特征 / 抽壳:按壳体厚度或相对面距离调整薄壁区域。",
|
||||
"壳体厚度修改本质上是移动当前平面或沿厚度方向缩放所属对象,需要相对面和厚度方向识别稳定。",
|
||||
)
|
||||
if is_plane:
|
||||
return (
|
||||
"柔性建模 / 拉伸切除 / 偏移变换:平面 Face 可按不同意图改变位置、面积或尺寸。",
|
||||
"平面 Face 不是直接暴露 B-Rep 参数;界面会把目标值映射为拉伸/切除、局部重建、移动特征或缩放特征。",
|
||||
)
|
||||
if is_cone:
|
||||
return (
|
||||
"工程特征 / 拔模或锥孔:只在简单圆锥或可识别锥孔上开放半角和参考半径。",
|
||||
"复杂圆锥面可能只是拔模、过渡或导入后的自由裁剪面,当前不会把不可靠参数伪装成可修改特征。",
|
||||
)
|
||||
if is_sphere:
|
||||
return (
|
||||
"解析曲面 / 缩放特征:球面半径通过缩放所属对象调整。",
|
||||
"当前不是恢复 CAD 历史里的球面特征,而是围绕识别到的中心做受限几何缩放。",
|
||||
)
|
||||
if is_torus:
|
||||
return (
|
||||
"解析曲面 / 缩放特征:环面主半径和小半径通过缩放所属对象调整。",
|
||||
"当前不是恢复 CAD 历史里的环面特征,而是围绕识别到的环面中心和轴线做受限几何缩放。",
|
||||
)
|
||||
if is_generic_surface:
|
||||
surface_name = _format_info_value("surface", surface)
|
||||
return (
|
||||
f"自由/复杂曲面:{surface_name} 当前只读,不开放参数化修改。",
|
||||
"自由曲面需要单独的曲面替换、重拟合或控制点编辑语义;当前一级阶段不把底层 UV 参数当成用户尺寸。",
|
||||
)
|
||||
if is_cylinder:
|
||||
return (
|
||||
"解析圆柱 / 拉伸切除:可按直径、高度或轴向缩放处理普通圆柱面。",
|
||||
"如果识别不到孔、槽、凸台或圆角语义,当前只按普通圆柱几何入口开放有限参数。",
|
||||
)
|
||||
return (
|
||||
"导入几何 Face:当前只显示可识别参数。",
|
||||
"STEP 通常没有完整建模历史;只有能映射到稳定 CAD 建模形式的参数才会开放修改。",
|
||||
)
|
||||
if has_edge:
|
||||
if curve == "line":
|
||||
return (
|
||||
"柔性建模 / 移动几何:直线 Edge 可改长度、端点,并作为倒角/倒圆角入口。",
|
||||
"Edge 长度有多种 CAD 语义:只改当前边、移动端面、相邻圆柱联动或缩放所属对象,界面会让用户选择。",
|
||||
)
|
||||
if curve == "circle":
|
||||
return (
|
||||
"工程特征入口 / 圆 Edge:可作为孔槽、圆角或相邻圆柱尺寸的参考。",
|
||||
"圆 Edge 本身通常不是独立历史特征;修改时会优先寻找相邻圆柱、孔槽或圆角语义。",
|
||||
)
|
||||
return (
|
||||
"曲线 Edge:当前只开放能稳定映射的尺寸或倒角/圆角入口。",
|
||||
"复杂曲线 Edge 暂不直接做自由形变,避免把拓扑边参数误当成 CAD 设计尺寸。",
|
||||
)
|
||||
if self.selected_solid_id is not None or self.selected_part_id is not None:
|
||||
return (
|
||||
"整体特征 / 移动几何 / 缩放特征:当前对象按整体变换处理。",
|
||||
"零件或 Solid 层级不代表单个 CAD 历史特征;当前只开放清晰的整体移动、旋转或缩放语义。",
|
||||
)
|
||||
return "", ""
|
||||
|
||||
def cad_recommended_operation() -> tuple[str, str]:
|
||||
if has_face:
|
||||
if is_hole_or_groove:
|
||||
if is_slot_or_half_hole:
|
||||
return (
|
||||
"优先改槽宽、槽深或槽孔总长度;要换位置时改轴心。",
|
||||
"槽类修改先走局部重建;如果用户选择缩放特征,会连同所属对象上的其它尺寸一起变化。",
|
||||
)
|
||||
return (
|
||||
"优先改孔径/半径;盲孔改深度;要换位置时改轴心。",
|
||||
"孔类修改会尽量先补旧孔再按目标重新切孔;螺纹孔、复杂孔组和底面识别不稳的盲孔仍可能受限。",
|
||||
)
|
||||
if is_boss:
|
||||
return (
|
||||
"优先改凸台直径或高度;要换位置时改轴心。",
|
||||
"凸台高度更接近拉伸/切除端盖;凸台直径和轴心更接近移除旧包络后重建局部凸台。",
|
||||
)
|
||||
if is_existing_fillet:
|
||||
return (
|
||||
"优先改圆角半径;复杂圆角链先不要用整体缩放替代。",
|
||||
"已有圆角需要先恢复支撑锐边再重新倒圆,支撑面不明确时失败回滚是正常保护。",
|
||||
)
|
||||
if is_shell_candidate:
|
||||
return (
|
||||
"优先改壳体厚度;普通位置变化再用偏移变换。",
|
||||
"壳体厚度需要相对面识别稳定;如果只想移动整个对象,请选择移动特征或缩放特征语义。",
|
||||
)
|
||||
if is_plane:
|
||||
return (
|
||||
"优先改偏移变换,并选择拉伸/切除;只想让当前面变形时再改面积、U/V尺寸或中心。",
|
||||
"拉伸/切除更接近常见 CAD 加料/切料;局部重建会让相邻面自然跟随变形,不一定保持垂直。",
|
||||
)
|
||||
if is_cone:
|
||||
return (
|
||||
"简单圆锥优先改半角或参考半径;复杂拔模面先保持只读。",
|
||||
"锥面可能来自拔模、沉孔或导入裁剪曲面,识别不稳时不直接开放大参数修改。",
|
||||
)
|
||||
if is_sphere:
|
||||
return (
|
||||
"优先改球面半径,并确认这是缩放所属对象。",
|
||||
"当前没有恢复球面历史特征,只能按识别中心做受限几何缩放。",
|
||||
)
|
||||
if is_torus:
|
||||
return (
|
||||
"优先改环面主半径或小半径,并确认这是缩放所属对象。",
|
||||
"当前没有恢复环面历史特征,只能按识别中心和轴线做受限几何缩放。",
|
||||
)
|
||||
if is_generic_surface:
|
||||
return (
|
||||
"当前建议只查看,不直接改自由曲面参数。",
|
||||
"自由曲面需要曲面替换、重拟合或控制点编辑,不能用面积或包围盒伪装成稳定 CAD 尺寸。",
|
||||
)
|
||||
if is_cylinder:
|
||||
return (
|
||||
"优先改直径或高度;若它其实是孔、槽、凸台或圆角,请先提高特征探测级别。",
|
||||
"普通圆柱入口是兜底几何编辑,不等同于已经识别到完整 CAD 特征。",
|
||||
)
|
||||
return (
|
||||
"先查看识别摘要,再只修改标记为可修改的参数。",
|
||||
"未映射到明确建模形式的 STEP Face 不应强行参数化。",
|
||||
)
|
||||
if has_edge:
|
||||
if curve == "line":
|
||||
return (
|
||||
"优先改长度;想保持端面垂直选移动端面,想只动当前边选只改当前Edge。",
|
||||
"同一条 Edge 改长可能对应多种 CAD 结果,当前必须显式选择建模意图。",
|
||||
)
|
||||
if curve == "circle":
|
||||
return (
|
||||
"优先让圆 Edge 关联到孔、槽、凸台或圆角直径,不直接自由变形这条边。",
|
||||
"圆 Edge 往往是相邻特征的边界,直接改边可能破坏孔槽或圆角语义。",
|
||||
)
|
||||
return (
|
||||
"只修改已明确开放的 Edge 参数;复杂曲线先保持只读。",
|
||||
"复杂曲线需要约束或曲线重拟合,不适合直接用边长驱动。",
|
||||
)
|
||||
if self.selected_solid_id is not None or self.selected_part_id is not None:
|
||||
return (
|
||||
"优先使用整体移动、旋转或缩放;不要把整体对象误当成单个历史特征。",
|
||||
"STEP 里零件/Solid 层级通常缺少装配约束和特征历史,整体操作要明确影响范围。",
|
||||
)
|
||||
return "", ""
|
||||
|
||||
cad_form_text, cad_form_tip = cad_modeling_form()
|
||||
if cad_form_text:
|
||||
add_readonly_spec(
|
||||
key="cad_modeling_form",
|
||||
label="建模形式",
|
||||
text=cad_form_text,
|
||||
tip=cad_form_tip,
|
||||
pin_top=True,
|
||||
)
|
||||
recommended_text, recommended_tip = cad_recommended_operation()
|
||||
if recommended_text:
|
||||
add_readonly_spec(
|
||||
key="cad_recommended_operation",
|
||||
label="推荐操作",
|
||||
text=recommended_text,
|
||||
tip=recommended_tip,
|
||||
pin_top=True,
|
||||
)
|
||||
|
||||
if has_face:
|
||||
topology_depth = _int_or_none(action_info.get("topology_relation_depth"))
|
||||
is_cylindrical_topology = bool(
|
||||
@@ -2225,7 +2516,7 @@ class WindowStateMixin:
|
||||
if is_slot_or_half_hole:
|
||||
add_readonly_spec(
|
||||
key="slot_edit_semantics",
|
||||
label="编辑方式",
|
||||
label="建模意图",
|
||||
text="槽/半孔:局部扇形槽重建;槽孔总长度会尝试配对另一端。",
|
||||
tip=(
|
||||
"槽/半孔宽度、深度和圆弧参数会换算成圆柱半径或圆弧角度,"
|
||||
@@ -2236,7 +2527,7 @@ class WindowStateMixin:
|
||||
else:
|
||||
add_readonly_spec(
|
||||
key="hole_edit_semantics",
|
||||
label="编辑方式",
|
||||
label="建模意图",
|
||||
text="孔:同轴重切孔壁;移动轴心会先填旧孔再切新孔。",
|
||||
tip=(
|
||||
"孔径/半径不是缩放整个模型,而是在当前孔轴线上做受限布尔重切;"
|
||||
@@ -2247,7 +2538,7 @@ class WindowStateMixin:
|
||||
elif is_boss:
|
||||
add_readonly_spec(
|
||||
key="boss_edit_semantics",
|
||||
label="编辑方式",
|
||||
label="建模意图",
|
||||
text="凸台:直径重建包络;高度拉伸/切除端盖;轴心先移除再补新凸台。",
|
||||
tip=(
|
||||
"凸台直径会通过局部布尔补料或移除旧包络后重建目标圆柱;"
|
||||
@@ -2258,7 +2549,7 @@ class WindowStateMixin:
|
||||
elif is_existing_fillet:
|
||||
add_readonly_spec(
|
||||
key="existing_fillet_edit_semantics",
|
||||
label="编辑方式",
|
||||
label="建模意图",
|
||||
text="圆角:移除已有圆角面,再尝试按目标半径重新倒圆。",
|
||||
tip=(
|
||||
"已有圆角修改不是恢复 CAD 历史参数;当前会先 defeature 当前圆角面,"
|
||||
@@ -2268,11 +2559,11 @@ class WindowStateMixin:
|
||||
elif is_cone or is_sphere or is_torus:
|
||||
add_readonly_spec(
|
||||
key="analytic_surface_edit_semantics",
|
||||
label="编辑方式",
|
||||
label="建模意图",
|
||||
text="解析曲面:围绕中心或轴线缩放所属对象,会影响同对象其它尺寸。",
|
||||
tip=(
|
||||
"圆锥、球面、环面这类曲面当前走几何缩放语义,"
|
||||
"不是只替换单个曲面的历史参数;执行前需要确认影响范围。"
|
||||
"不是只替换单个曲面的历史参数;执行前需要确认建模意图。"
|
||||
),
|
||||
)
|
||||
elif is_generic_surface:
|
||||
@@ -2284,7 +2575,7 @@ class WindowStateMixin:
|
||||
)
|
||||
add_readonly_spec(
|
||||
key="freeform_surface_edit_semantics",
|
||||
label="编辑方式",
|
||||
label="建模意图",
|
||||
text=f"{surface_name}:暂不支持参数化编辑。",
|
||||
tip=(
|
||||
f"{blocker} 当前不会把自由曲面的面积、中心、包围盒或底层 UV 参数伪装成可修改尺寸;"
|
||||
@@ -2311,11 +2602,11 @@ class WindowStateMixin:
|
||||
),
|
||||
tip=topology_tip,
|
||||
)
|
||||
face_semantics_text = "Face:先改目标值,再用“影响范围”选择局部重建、拉伸/切除、移动特征或缩放特征。"
|
||||
face_semantics_text = "Face:先改目标值,再用“建模意图”选择局部重建、拉伸/切除、移动特征或缩放特征。"
|
||||
face_semantics_tip = (
|
||||
"面积是 Face 的几何面积;U向尺寸/V向尺寸是这个 Face 自身参数方向上的两个投影尺寸;"
|
||||
"偏移曲面使用 Creo 曲面 Offset 语义,表示沿当前 Face 法向得到的目标位置。"
|
||||
"影响范围决定这次修改是局部重建当前 Face、按拉伸/切除加减材料,"
|
||||
"偏移变换使用 Creo 柔性建模里的 Offset 语义,表示沿当前 Face 法向得到的目标位置。"
|
||||
"建模意图决定这次修改是局部重建当前 Face、按拉伸/切除加减材料,"
|
||||
"还是移动或缩放所属特征/Solid。"
|
||||
)
|
||||
if is_plane and not local_face_deform_ready:
|
||||
@@ -2327,7 +2618,7 @@ class WindowStateMixin:
|
||||
)
|
||||
add_readonly_spec(
|
||||
key="face_edit_semantics",
|
||||
label="编辑方式",
|
||||
label="建模意图",
|
||||
text=face_semantics_text,
|
||||
tip=face_semantics_tip,
|
||||
)
|
||||
@@ -2335,10 +2626,10 @@ class WindowStateMixin:
|
||||
if has_edge:
|
||||
add_readonly_spec(
|
||||
key="edge_edit_semantics",
|
||||
label="编辑方式",
|
||||
label="建模意图",
|
||||
text="Edge:长度修改可选择局部形变、移动端面或缩放所属对象。",
|
||||
tip=(
|
||||
"只变当前Edge会重建局部相邻面;移动端面更像整体尺寸变化;"
|
||||
"只改当前Edge会重建局部相邻面;移动端面更像整体尺寸变化;"
|
||||
"缩放所属会影响同一特征或 Solid 上的其它尺寸。"
|
||||
),
|
||||
)
|
||||
@@ -2571,7 +2862,7 @@ class WindowStateMixin:
|
||||
)
|
||||
add_scoped_spec(
|
||||
key="face_target_normal_position",
|
||||
label="偏移曲面",
|
||||
label="偏移变换",
|
||||
current_raw=current_plane_position if current_plane_position is not None else "",
|
||||
target_text=numeric_text(current_plane_position),
|
||||
scope_default="push_pull",
|
||||
@@ -2581,10 +2872,10 @@ class WindowStateMixin:
|
||||
"action": "push_pull_face",
|
||||
"target_attr": "offset_input",
|
||||
"enabled": current_plane_position is not None,
|
||||
"enabled_tip": "输入偏移曲面的目标位置;程序会沿当前 Face 法向执行拉伸或切除。",
|
||||
"enabled_tip": "输入偏移变换的目标位置;程序会沿当前 Face 法向执行拉伸或切除。",
|
||||
"disabled_tip": "当前平面缺少稳定移动方向或基准点,不能按目标位置拉伸/切除。",
|
||||
"range_hint": (
|
||||
"偏移曲面是沿当前 Face 法向测量的目标位置,不是面积、不是移动距离,也不是 X/Y/Z 坐标;单位同模型。"
|
||||
"偏移变换是沿当前 Face 法向测量的目标位置,不是面积、不是移动距离,也不是 X/Y/Z 坐标;单位同模型。"
|
||||
"程序会把目标位置自动换算成本次拉伸/切除距离。"
|
||||
f"{face_offset_hard_limit}"
|
||||
),
|
||||
@@ -2602,11 +2893,11 @@ class WindowStateMixin:
|
||||
and current_face_center is not None
|
||||
),
|
||||
"enabled_tip": (
|
||||
"输入偏移曲面的目标位置;程序只把当前 Face 沿法向移动到该目标值,"
|
||||
"输入偏移变换的目标位置;程序只把当前 Face 沿法向移动到该目标值,"
|
||||
"并让相邻平面按新顶点重建。"
|
||||
),
|
||||
"disabled_tip": face_local_disabled_tip(
|
||||
"当前平面缺少稳定方向、基准点或中心,不能按偏移曲面局部重建当前 Face。"
|
||||
"当前平面缺少稳定方向、基准点或中心,不能按偏移变换局部重建当前 Face。"
|
||||
),
|
||||
"range_hint": (
|
||||
"局部重建不会自动加料/切削,相邻面可能自然变斜,"
|
||||
@@ -2626,10 +2917,10 @@ class WindowStateMixin:
|
||||
and (self.selected_part_id is not None or self.selected_solid_id is not None)
|
||||
),
|
||||
"enabled_tip": (
|
||||
"输入偏移曲面的目标位置;程序会沿当前 Face 法向平移所属特征或 Solid,"
|
||||
"输入偏移变换的目标位置;程序会沿当前 Face 法向平移所属特征或 Solid,"
|
||||
"当前面形状和所属对象内部尺寸不变。"
|
||||
),
|
||||
"disabled_tip": "当前平面缺少稳定方向、基准点或所属对象,不能按偏移曲面移动所属特征。",
|
||||
"disabled_tip": "当前平面缺少稳定方向、基准点或所属对象,不能按偏移变换移动所属特征。",
|
||||
"range_hint": (
|
||||
"这是移动所属对象,不是拉伸/切除当前 Face;如果想改变形状或厚度,请选择“拉伸/切除”。"
|
||||
f"{translation_hint()} {face_offset_hard_limit}"
|
||||
@@ -2682,7 +2973,7 @@ class WindowStateMixin:
|
||||
"disabled_tip": "当前壳体候选缺少稳定厚度、厚度方向、基准平面或所属对象,不能按厚度缩放所属特征。",
|
||||
"range_hint": (
|
||||
"这是缩放所属对象,不是单独移动当前平面;"
|
||||
f"如果只想移动当前壳体平面区域,请把影响范围设为“拉伸/切除”。 {relative_range_hint(current, 0.2, 0.5, face_linear_hard_limit)}"
|
||||
f"如果只想移动当前壳体平面区域,请把建模意图设为“拉伸/切除”。 {relative_range_hint(current, 0.2, 0.5, face_linear_hard_limit)}"
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -4788,7 +5079,7 @@ class WindowStateMixin:
|
||||
self._update_action_states()
|
||||
self._refresh_property_editor()
|
||||
label = self.edge_length_strategy_combo.currentText() if hasattr(self, "edge_length_strategy_combo") else ""
|
||||
self.statusBar().showMessage(f"已设置修改策略:{label or '自动策略'}。")
|
||||
self.statusBar().showMessage(f"已设置建模意图:{label or '自动选择'}。")
|
||||
|
||||
def set_chamfer_reference_face(self) -> None:
|
||||
self._update_action_states()
|
||||
@@ -5126,6 +5417,7 @@ class WindowStateMixin:
|
||||
return result
|
||||
if self.selected_kind == "edge" and self.selected_edge_id is not None:
|
||||
result = self.model.edge_info(self.selected_edge_id)
|
||||
result.update(self._edge_first_level_selection_fields(self.selected_edge_id))
|
||||
self._selected_action_info_cache_key = cache_key
|
||||
self._selected_action_info_cache_value = dict(result)
|
||||
return result
|
||||
@@ -5200,6 +5492,7 @@ class WindowStateMixin:
|
||||
resolved_edge_id = self._resolve_record_edge_id(record)
|
||||
if resolved_edge_id is not None:
|
||||
info = self.model.edge_info(resolved_edge_id)
|
||||
info.update(self._edge_first_level_selection_fields(resolved_edge_id))
|
||||
self.selected_kind = "edge"
|
||||
self.selected_edge_id = resolved_edge_id
|
||||
self.selected_part_id = int(info["part_id"])
|
||||
|
||||
Reference in New Issue
Block a user