feat: 完善 Edge 一级编辑与 CAD 建模语义

This commit is contained in:
2026-08-05 18:06:15 +08:00
parent ed1faf51d8
commit 57d75541c3
19 changed files with 2365 additions and 367 deletions
+7 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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]],
+41 -3
View File
@@ -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": "切削警告",
+64 -64
View File
@@ -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,不做单面局部扭曲。"
+1
View File
@@ -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
View File
@@ -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"])