feat: 完善一级关系编辑 UI 与视图体验
This commit is contained in:
+521
-107
@@ -1678,6 +1678,23 @@ class OperationMixin:
|
||||
notes.append(f"相邻圆柱Face {face_id} 的{mode_label}不可用:{axis_plan.get('message', '')}")
|
||||
continue
|
||||
|
||||
axis_topology_fields = {
|
||||
f"move_axis_{key}": axis_plan.get(key)
|
||||
for key in (
|
||||
"topology_relation_depth",
|
||||
"topology_relation_status",
|
||||
"topology_relation_boundary",
|
||||
"topology_ignored_relation_depths",
|
||||
"first_level_topology_status",
|
||||
"first_level_topology_guard_note",
|
||||
"first_level_boundary_edge_count",
|
||||
"first_level_adjacent_face_count",
|
||||
"cylindrical_feature_side_face_count",
|
||||
"cylindrical_feature_boundary_edge_count",
|
||||
"cylindrical_feature_adjacent_face_count",
|
||||
)
|
||||
if key in axis_plan
|
||||
}
|
||||
candidate = {
|
||||
"circular_edge_cylinder_face_id": face_id,
|
||||
"circular_edge_cylinder_mode": mode,
|
||||
@@ -1703,6 +1720,7 @@ class OperationMixin:
|
||||
"move_axis_blockers": axis_plan.get("blockers", ""),
|
||||
"move_axis_feature_guess": axis_plan.get("feature_guess", feature_guess),
|
||||
"move_axis_confidence": axis_plan.get("confidence", face_info.get("confidence", "")),
|
||||
**axis_topology_fields,
|
||||
"resize_strategy": "move-adjacent-cylinder-from-circular-edge-center",
|
||||
"edit_strategy_label": f"通过{mode_label}移动圆Edge",
|
||||
"edit_semantics": (
|
||||
@@ -2347,11 +2365,53 @@ class OperationMixin:
|
||||
feature_guess = str(info.get("feature_guess", ""))
|
||||
current_radius = float(feature.get("existing_fillet_radius_estimate", info["radius"]))
|
||||
support_face_ids = tuple(feature.get("feature_existing_fillet_support_face_ids", ()))
|
||||
topology_fields = self._cylindrical_feature_first_level_plan_fields(face_id)
|
||||
warnings: list[str] = []
|
||||
blockers: list[str] = []
|
||||
risk = "medium"
|
||||
status = "caution"
|
||||
|
||||
topology_status = str(topology_fields.get("topology_relation_status") or "")
|
||||
side_count = int(topology_fields.get("cylindrical_feature_side_face_count", 0) or 0)
|
||||
boundary_edge_count = int(topology_fields.get("cylindrical_feature_boundary_edge_count", 0) or 0)
|
||||
adjacent_face_ids = tuple(_int_values(topology_fields.get("cylindrical_feature_adjacent_face_ids")))
|
||||
adjacent_count = int(topology_fields.get("cylindrical_feature_adjacent_face_count", 0) or 0)
|
||||
missing_support_faces = tuple(sorted(set(support_face_ids) - set(adjacent_face_ids)))
|
||||
topology_blockers: list[str] = []
|
||||
topology_warnings: list[str] = []
|
||||
topology_risk = "low"
|
||||
if topology_status != "ready":
|
||||
topology_blockers.append("当前圆角的一级拓扑关系无法确认,不能稳定执行移除旧圆角并重建。")
|
||||
if side_count <= 0:
|
||||
topology_blockers.append("没有识别到当前圆角面区域。")
|
||||
if boundary_edge_count <= 0:
|
||||
topology_blockers.append("没有识别到当前圆角面的边界 Edge,不能确认重建影响范围。")
|
||||
if adjacent_count < 2:
|
||||
topology_blockers.append("没有识别到当前圆角直接连接的两个支撑 Face。")
|
||||
if missing_support_faces:
|
||||
topology_blockers.append(f"已识别支撑 Face {missing_support_faces} 不在当前圆角一级相邻 Face 中。")
|
||||
if adjacent_count > 8 or boundary_edge_count > 16:
|
||||
topology_risk = _max_risk(topology_risk, "high")
|
||||
topology_warnings.append("当前圆角一级邻域较复杂,重建后需要重点检查相邻面。")
|
||||
elif adjacent_count > 4 or boundary_edge_count > 8:
|
||||
topology_risk = _max_risk(topology_risk, "medium")
|
||||
topology_warnings.append("当前圆角一级邻域包含较多相邻面或边界边。")
|
||||
topology_fields.update(
|
||||
{
|
||||
"first_level_topology_status": "blocked" if topology_blockers else "ready",
|
||||
"first_level_topology_risk": "blocked" if topology_blockers else topology_risk,
|
||||
"first_level_topology_blockers": ";".join(topology_blockers),
|
||||
"first_level_topology_warnings": ";".join(topology_warnings),
|
||||
"first_level_topology_guard_note": (
|
||||
f"圆角一级关系检查:圆角 Face {side_count} 个,边界 Edge {boundary_edge_count} 条,"
|
||||
f"直接相邻 Face {adjacent_count} 个,支撑 Face {len(support_face_ids)} 个。"
|
||||
),
|
||||
}
|
||||
)
|
||||
blockers.extend(topology_blockers)
|
||||
warnings.extend(topology_warnings)
|
||||
risk = _max_risk(risk, str(topology_fields.get("first_level_topology_risk") or "low"))
|
||||
|
||||
if feature_guess != "round/fillet candidate":
|
||||
blockers.append("当前圆柱面没有被识别为已有圆角/倒圆候选。")
|
||||
if target_radius <= 0:
|
||||
@@ -2412,6 +2472,7 @@ class OperationMixin:
|
||||
"face_id": face_id,
|
||||
"part_id": info.get("part_id"),
|
||||
"solid_id": info.get("solid_id"),
|
||||
**topology_fields,
|
||||
"feature_type": feature.get("feature_type"),
|
||||
"feature_guess": feature_guess,
|
||||
"confidence": info.get("confidence"),
|
||||
@@ -4105,6 +4166,7 @@ class OperationMixin:
|
||||
blockers.append(f"当前选中 Face 不是圆柱面,不能调整{edit_label}。")
|
||||
if require_boss and str(info.get("feature_guess", "")) != "boss/outer-round candidate":
|
||||
blockers.append("凸台高度调整当前版本只支持明确的圆柱凸台候选。")
|
||||
risk = self._apply_cylindrical_first_level_guard(topology_fields, blockers, warnings, risk)
|
||||
cylinder_identity = {**info, **feature}
|
||||
angular_span = _effective_cylinder_angular_span(cylinder_identity) or 0.0
|
||||
if not _is_effectively_full_cylinder(cylinder_identity):
|
||||
@@ -4239,6 +4301,7 @@ class OperationMixin:
|
||||
"face_id": face_id,
|
||||
"part_id": info.get("part_id"),
|
||||
"solid_id": info.get("solid_id"),
|
||||
**topology_fields,
|
||||
"feature_type": feature.get("feature_type"),
|
||||
"feature_guess": info.get("feature_guess"),
|
||||
"confidence": info.get("confidence"),
|
||||
@@ -4320,7 +4383,8 @@ class OperationMixin:
|
||||
f"target_height={float(plan['target_height']):g}, "
|
||||
f"delta={float(plan['delta_height']):g}, "
|
||||
f"push_pull_distance={float(plan['push_pull_distance']):g}, "
|
||||
f"risk={plan['risk']}, verified_face={verification.get('face_id', '')}. {push_result}"
|
||||
f"risk={plan['risk']}, verified_face={verification.get('face_id', '')}, "
|
||||
f"first_level_topology_matched={verification.get('first_level_topology_matched', '')}. {push_result}"
|
||||
)
|
||||
|
||||
def shell_thickness_preview_polydata(
|
||||
@@ -7664,6 +7728,7 @@ class OperationMixin:
|
||||
check = self._edge_length_result_check(plan)
|
||||
if check is None:
|
||||
return "Result check unavailable."
|
||||
topology_check = self._edge_first_level_result_summary(plan, int(check["edge_id"]), enforce=False)
|
||||
return (
|
||||
"Result check: "
|
||||
f"match={check['match_method']}, "
|
||||
@@ -7672,7 +7737,7 @@ class OperationMixin:
|
||||
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']}."
|
||||
f"scope={check['scope']}. {topology_check}"
|
||||
)
|
||||
|
||||
def _edge_length_result_summary_or_raise(self, plan: dict[str, object]) -> str:
|
||||
@@ -7699,6 +7764,7 @@ class OperationMixin:
|
||||
f"允许误差 {endpoint_tolerance:g}。"
|
||||
)
|
||||
|
||||
topology_check = self._edge_first_level_result_summary(plan, int(check["edge_id"]), enforce=True)
|
||||
return (
|
||||
"Result check: "
|
||||
f"match={check['match_method']}, "
|
||||
@@ -7707,7 +7773,173 @@ class OperationMixin:
|
||||
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']}."
|
||||
f"scope={check['scope']}. {topology_check}"
|
||||
)
|
||||
|
||||
def _edge_first_level_result_summary(
|
||||
self,
|
||||
plan: dict[str, object],
|
||||
result_edge_id: int,
|
||||
*,
|
||||
enforce: bool,
|
||||
) -> str:
|
||||
strategy = str(plan.get("resize_strategy") or "")
|
||||
enforce_strategy = strategy in {
|
||||
"local-edge-only-deform",
|
||||
"local-edge-endpoint-deform",
|
||||
"local-edge-center-deform",
|
||||
"move-edge-end-plane-by-push-pull",
|
||||
}
|
||||
expected_vertices = _int_or_none(plan.get("first_level_fact_boundary_vertex_count"))
|
||||
if expected_vertices is None:
|
||||
expected_vertices = _int_or_none(plan.get("first_level_vertex_count"))
|
||||
expected_adjacent_edges = _int_or_none(plan.get("first_level_fact_adjacent_edge_count"))
|
||||
if expected_adjacent_edges is None:
|
||||
expected_adjacent_edges = _int_or_none(plan.get("first_level_adjacent_edge_count"))
|
||||
expected_adjacent_faces = _int_or_none(plan.get("first_level_fact_adjacent_face_count"))
|
||||
if expected_adjacent_faces is None:
|
||||
expected_adjacent_faces = _int_or_none(plan.get("first_level_adjacent_face_count"))
|
||||
expected_included_edges = _int_or_none(plan.get("first_level_fact_included_edge_count"))
|
||||
if expected_included_edges is None:
|
||||
expected_included_edges = _int_or_none(plan.get("first_level_edge_count"))
|
||||
|
||||
if not any(
|
||||
value is not None and value > 0
|
||||
for value in (expected_vertices, expected_adjacent_edges, expected_adjacent_faces, expected_included_edges)
|
||||
):
|
||||
return "First-level topology check: unavailable."
|
||||
|
||||
try:
|
||||
topology = self.edge_first_level_topology(int(result_edge_id))
|
||||
except Exception as exc:
|
||||
if enforce and enforce_strategy:
|
||||
raise RuntimeError(
|
||||
"Edge编辑后无法重新识别目标Edge的一级关系,模型已恢复到修改前状态。"
|
||||
f"目标Edge {result_edge_id},原因:{exc}"
|
||||
) from exc
|
||||
return f"First-level topology check: unavailable ({exc})."
|
||||
|
||||
actual_vertices = int(topology.get("first_level_vertex_count", 0) or 0)
|
||||
actual_adjacent_edges = int(topology.get("first_level_adjacent_edge_count", 0) or 0)
|
||||
actual_adjacent_faces = int(topology.get("first_level_adjacent_face_count", 0) or 0)
|
||||
actual_included_edges = int(topology.get("first_level_edge_count", 0) or 0)
|
||||
|
||||
failures: list[str] = []
|
||||
if expected_vertices is not None and expected_vertices > 0 and actual_vertices != expected_vertices:
|
||||
failures.append(f"端点 Vertex {actual_vertices}/{expected_vertices}")
|
||||
if (
|
||||
expected_adjacent_edges is not None
|
||||
and expected_adjacent_edges > 0
|
||||
and actual_adjacent_edges < expected_adjacent_edges
|
||||
):
|
||||
failures.append(f"共享端点相邻 Edge {actual_adjacent_edges}/{expected_adjacent_edges}")
|
||||
if (
|
||||
expected_adjacent_faces is not None
|
||||
and expected_adjacent_faces > 0
|
||||
and actual_adjacent_faces < expected_adjacent_faces
|
||||
):
|
||||
failures.append(f"直接相邻 Face {actual_adjacent_faces}/{expected_adjacent_faces}")
|
||||
if (
|
||||
expected_included_edges is not None
|
||||
and expected_included_edges > 0
|
||||
and actual_included_edges < expected_included_edges
|
||||
):
|
||||
failures.append(f"一级范围 Edge {actual_included_edges}/{expected_included_edges}")
|
||||
|
||||
if failures and enforce and enforce_strategy:
|
||||
raise RuntimeError(
|
||||
"Edge编辑后一级关系不完整,模型已恢复到修改前状态。"
|
||||
f"目标Edge {result_edge_id},未通过:{'; '.join(failures)}。"
|
||||
)
|
||||
|
||||
status = "ok" if not failures else f"warning({'; '.join(failures)})"
|
||||
return (
|
||||
"First-level topology check: "
|
||||
f"{status}, edge={result_edge_id}, endpoint_vertices={actual_vertices}, "
|
||||
f"adjacent_edges={actual_adjacent_edges}, adjacent_faces={actual_adjacent_faces}, "
|
||||
f"included_edges={actual_included_edges}."
|
||||
)
|
||||
|
||||
def _edge_blend_result_summary_or_raise(
|
||||
self,
|
||||
plan: dict[str, object],
|
||||
before_stats,
|
||||
*,
|
||||
mode: str,
|
||||
) -> str:
|
||||
part_id = _int_or_none(plan.get("part_id"))
|
||||
if part_id is None:
|
||||
raise RuntimeError("Edge blend result check failed: the source part could not be resolved.")
|
||||
part = self.part_by_id(part_id)
|
||||
if part is None:
|
||||
raise RuntimeError(f"Edge blend result check failed: unknown part id {part_id}.")
|
||||
|
||||
_ensure_valid_shape(part.shape)
|
||||
after_stats = self.part_topology_stats(part_id)
|
||||
before_solids = int(getattr(before_stats, "solids", 0))
|
||||
after_solids = int(getattr(after_stats, "solids", 0))
|
||||
if before_solids > 0 and after_solids != before_solids:
|
||||
raise RuntimeError(
|
||||
"Edge blend result changed the solid count; the model has been restored to the previous state. "
|
||||
f"Solids {before_solids}->{after_solids}."
|
||||
)
|
||||
|
||||
face_delta = int(getattr(after_stats, "faces", 0)) - int(getattr(before_stats, "faces", 0))
|
||||
edge_delta = int(getattr(after_stats, "edges", 0)) - int(getattr(before_stats, "edges", 0))
|
||||
if mode == "fillet":
|
||||
target_radius = _float_or_none(plan.get("target_radius"))
|
||||
if target_radius is None or target_radius <= 0.0:
|
||||
raise RuntimeError("Edge fillet result check failed: target radius is invalid.")
|
||||
tolerance = max(_shape_diagonal(part.shape) * 1e-5, target_radius * 1e-3, 1e-5)
|
||||
matched_faces: list[int] = []
|
||||
for face_id in range(len(self.faces)):
|
||||
if int(self.face_part_ids[face_id]) != part_id:
|
||||
continue
|
||||
try:
|
||||
info = self.face_info(face_id)
|
||||
except Exception:
|
||||
continue
|
||||
if info.get("surface") != "cylinder":
|
||||
continue
|
||||
radius = _float_or_none(info.get("radius"))
|
||||
if radius is None:
|
||||
continue
|
||||
if abs(radius - target_radius) <= tolerance:
|
||||
matched_faces.append(face_id)
|
||||
if not matched_faces:
|
||||
raise RuntimeError(
|
||||
"Edge fillet result did not contain a cylindrical face near the target radius; "
|
||||
"the model has been restored to the previous state. "
|
||||
f"Target radius {target_radius:g}, tolerance {tolerance:g}."
|
||||
)
|
||||
if face_delta < 0:
|
||||
raise RuntimeError(
|
||||
"Edge fillet result lost faces unexpectedly; the model has been restored to the previous state. "
|
||||
f"Face delta {face_delta}."
|
||||
)
|
||||
return (
|
||||
"Edge blend result check: "
|
||||
f"mode=fillet, solids={before_solids}->{after_solids}, "
|
||||
f"face_delta={face_delta}, edge_delta={edge_delta}, "
|
||||
f"target_radius={target_radius:g}, matched_faces={tuple(matched_faces[:8])}."
|
||||
)
|
||||
|
||||
if face_delta <= 0:
|
||||
raise RuntimeError(
|
||||
"Edge chamfer result did not add the expected chamfer face; "
|
||||
"the model has been restored to the previous state. "
|
||||
f"Face delta {face_delta}."
|
||||
)
|
||||
if edge_delta <= 0:
|
||||
raise RuntimeError(
|
||||
"Edge chamfer result did not add the expected boundary edges; "
|
||||
"the model has been restored to the previous state. "
|
||||
f"Edge delta {edge_delta}."
|
||||
)
|
||||
return (
|
||||
"Edge blend result check: "
|
||||
f"mode={mode}, solids={before_solids}->{after_solids}, "
|
||||
f"face_delta={face_delta}, edge_delta={edge_delta}."
|
||||
)
|
||||
|
||||
def _edge_length_result_check(self, plan: dict[str, object]) -> dict[str, object] | None:
|
||||
@@ -10166,6 +10398,119 @@ class OperationMixin:
|
||||
return False
|
||||
return bool(saw_outer_radius and saw_inner_radius)
|
||||
|
||||
def _existing_fillet_result_summary_or_raise(
|
||||
self,
|
||||
plan: dict[str, object],
|
||||
before_stats,
|
||||
) -> str:
|
||||
part_id = _int_or_none(plan.get("part_id"))
|
||||
if part_id is None:
|
||||
raise RuntimeError("Existing fillet result check failed: the source part could not be resolved.")
|
||||
part = self.part_by_id(part_id)
|
||||
if part is None:
|
||||
raise RuntimeError(f"Existing fillet result check failed: unknown part id {part_id}.")
|
||||
|
||||
_ensure_valid_shape(part.shape)
|
||||
after_stats = self.part_topology_stats(part_id)
|
||||
before_solids = int(getattr(before_stats, "solids", 0))
|
||||
after_solids = int(getattr(after_stats, "solids", 0))
|
||||
if before_solids > 0 and after_solids != before_solids:
|
||||
raise RuntimeError(
|
||||
"Existing fillet result changed the solid count; the model has been restored to the previous state. "
|
||||
f"Solids {before_solids}->{after_solids}."
|
||||
)
|
||||
|
||||
target_radius = _float_or_none(plan.get("target_radius"))
|
||||
if target_radius is None or target_radius <= 0.0:
|
||||
raise RuntimeError("Existing fillet result check failed: target radius is invalid.")
|
||||
diagonal = max(_shape_diagonal(part.shape), target_radius, 1.0)
|
||||
radius_tolerance = max(target_radius * 0.01, diagonal * 1e-5, 1e-5)
|
||||
required_adjacent = max(2, min(int(plan.get("cylindrical_feature_adjacent_face_count", 0) or 0), 4))
|
||||
required_boundary = max(1, min(int(plan.get("cylindrical_feature_boundary_edge_count", 0) or 0), 4))
|
||||
matched: list[dict[str, object]] = []
|
||||
closest: dict[str, object] | None = None
|
||||
|
||||
for candidate_face_id in range(len(self.faces)):
|
||||
if int(self.face_part_ids[candidate_face_id]) != part_id:
|
||||
continue
|
||||
try:
|
||||
info = self.face_info(candidate_face_id)
|
||||
except Exception:
|
||||
continue
|
||||
if info.get("surface") != "cylinder":
|
||||
continue
|
||||
radius = _float_or_none(info.get("radius"))
|
||||
if radius is None:
|
||||
continue
|
||||
radius_error = abs(radius - target_radius)
|
||||
candidate = {
|
||||
"face_id": candidate_face_id,
|
||||
"radius": radius,
|
||||
"radius_error": radius_error,
|
||||
"feature_guess": info.get("feature_guess", ""),
|
||||
"angular_span": info.get("angular_span", ""),
|
||||
}
|
||||
if closest is None or radius_error < float(closest["radius_error"]):
|
||||
closest = candidate
|
||||
if radius_error > radius_tolerance:
|
||||
continue
|
||||
try:
|
||||
topology = self.cylindrical_feature_first_level_topology(candidate_face_id)
|
||||
side_count = int(topology.get("cylindrical_feature_side_face_count", 0) or 0)
|
||||
boundary_count = int(topology.get("cylindrical_feature_boundary_edge_count", 0) or 0)
|
||||
adjacent_count = int(topology.get("cylindrical_feature_adjacent_face_count", 0) or 0)
|
||||
except Exception as exc:
|
||||
raise RuntimeError(
|
||||
"Existing fillet result matched the target radius but first-level topology failed; "
|
||||
f"Face {candidate_face_id}, error={exc}."
|
||||
) from exc
|
||||
if side_count <= 0 or boundary_count < required_boundary or adjacent_count < required_adjacent:
|
||||
raise RuntimeError(
|
||||
"Existing fillet result matched the target radius but lost first-level topology; "
|
||||
f"Face {candidate_face_id}, side={side_count}, boundary_edges={boundary_count}, "
|
||||
f"adjacent_faces={adjacent_count}, required_boundary={required_boundary}, "
|
||||
f"required_adjacent={required_adjacent}."
|
||||
)
|
||||
matched.append(
|
||||
{
|
||||
**candidate,
|
||||
"side_face_count": side_count,
|
||||
"boundary_edge_count": boundary_count,
|
||||
"adjacent_face_count": adjacent_count,
|
||||
}
|
||||
)
|
||||
|
||||
if not matched:
|
||||
if closest is None:
|
||||
detail = "no cylindrical Face remained in the source part"
|
||||
else:
|
||||
detail = (
|
||||
f"closest Face {closest['face_id']} radius {float(closest['radius']):.6g}, "
|
||||
f"target {target_radius:.6g}, error {float(closest['radius_error']):.6g}"
|
||||
)
|
||||
raise RuntimeError(
|
||||
"Existing fillet result did not contain a target-radius cylindrical round Face; "
|
||||
f"the model has been restored to the previous state. {detail}."
|
||||
)
|
||||
|
||||
face_delta = int(getattr(after_stats, "faces", 0)) - int(getattr(before_stats, "faces", 0))
|
||||
edge_delta = int(getattr(after_stats, "edges", 0)) - int(getattr(before_stats, "edges", 0))
|
||||
matched_face_ids = tuple(int(item["face_id"]) for item in matched[:8])
|
||||
topology_summaries = tuple(
|
||||
{
|
||||
"face_id": int(item["face_id"]),
|
||||
"boundary_edge_count": int(item["boundary_edge_count"]),
|
||||
"adjacent_face_count": int(item["adjacent_face_count"]),
|
||||
}
|
||||
for item in matched[:8]
|
||||
)
|
||||
return (
|
||||
"Existing fillet result check: "
|
||||
f"solids={before_solids}->{after_solids}, face_delta={face_delta}, edge_delta={edge_delta}, "
|
||||
f"target_radius={target_radius:g}, matched_faces={matched_face_ids}, "
|
||||
f"first_level_topology_matched=True, topology={topology_summaries}."
|
||||
)
|
||||
|
||||
def resize_existing_fillet(self, face_id: int, target_radius: float) -> str:
|
||||
plan = self.existing_fillet_resize_plan(face_id, target_radius)
|
||||
if plan["status"] == "blocked":
|
||||
@@ -10176,49 +10521,58 @@ class OperationMixin:
|
||||
if part is None:
|
||||
raise ValueError(f"Unknown part id {part_id}")
|
||||
|
||||
source_face = topods.Face(self.faces[face_id])
|
||||
defeatured = _defeature_faces(part.shape, [source_face])
|
||||
axis_point = gp_Pnt(*plan["axis_point"])
|
||||
axis_dir = gp_Dir(*plan["axis"])
|
||||
root_edges = _axis_aligned_edge_candidates(
|
||||
defeatured,
|
||||
axis_point,
|
||||
axis_dir,
|
||||
expected_length=float(plan.get("height_estimate") or 0.0),
|
||||
reference_radius=float(plan["current_radius"]),
|
||||
)
|
||||
if not root_edges:
|
||||
raise RuntimeError(
|
||||
"已尝试移除已有圆角面,但没有找到可重新倒圆的轴向锐边;"
|
||||
"该圆角可能不是简单直线边圆角。"
|
||||
old_part_shape = part.shape
|
||||
before_stats = self.part_topology_stats(part_id)
|
||||
try:
|
||||
source_face = topods.Face(self.faces[face_id])
|
||||
defeatured = _defeature_faces(part.shape, [source_face])
|
||||
axis_point = gp_Pnt(*plan["axis_point"])
|
||||
axis_dir = gp_Dir(*plan["axis"])
|
||||
root_edges = _axis_aligned_edge_candidates(
|
||||
defeatured,
|
||||
axis_point,
|
||||
axis_dir,
|
||||
expected_length=float(plan.get("height_estimate") or 0.0),
|
||||
reference_radius=float(plan["current_radius"]),
|
||||
)
|
||||
if not root_edges:
|
||||
raise RuntimeError(
|
||||
"Existing fillet face was removed, but no axis-aligned sharp Edge was found for refillet; "
|
||||
"the selected round is probably not a simple straight-edge fillet."
|
||||
)
|
||||
|
||||
result = None
|
||||
failures: list[str] = []
|
||||
for index, root_edge in enumerate(root_edges[:16], start=1):
|
||||
try:
|
||||
maker = BRepFilletAPI_MakeFillet(defeatured)
|
||||
maker.Add(float(target_radius), topods.Edge(root_edge))
|
||||
result = _finalize_builder_result(maker, f"existing fillet resize candidate {index}")
|
||||
break
|
||||
except Exception as exc:
|
||||
failures.append(str(exc))
|
||||
if result is None:
|
||||
detail = failures[-1] if failures else "没有可用的候选边。"
|
||||
raise RuntimeError(
|
||||
"已移除已有圆角面,但所有候选锐边都无法重新倒圆;"
|
||||
f"该圆角可能是复杂 blend 或支撑面不适合重建。最后错误:{detail}"
|
||||
)
|
||||
result = None
|
||||
failures: list[str] = []
|
||||
for index, root_edge in enumerate(root_edges[:16], start=1):
|
||||
try:
|
||||
maker = BRepFilletAPI_MakeFillet(defeatured)
|
||||
maker.Add(float(target_radius), topods.Edge(root_edge))
|
||||
result = _finalize_builder_result(maker, f"existing fillet resize candidate {index}")
|
||||
break
|
||||
except Exception as exc:
|
||||
failures.append(str(exc))
|
||||
if result is None:
|
||||
detail = failures[-1] if failures else "no candidate Edge was available"
|
||||
raise RuntimeError(
|
||||
"Existing fillet face was removed, but every candidate sharp Edge failed to refillet; "
|
||||
f"the selected round may be a complex blend. Last error: {detail}"
|
||||
)
|
||||
|
||||
part.shape = result
|
||||
self.refresh_topology()
|
||||
result_check = self._existing_fillet_result_summary_or_raise(plan, before_stats)
|
||||
except Exception:
|
||||
part.shape = old_part_shape
|
||||
self.refresh_topology()
|
||||
raise
|
||||
|
||||
part.shape = result
|
||||
self.refresh_topology()
|
||||
return (
|
||||
"Existing fillet radius resize completed: "
|
||||
f"face {face_id}, current_radius={float(plan['current_radius']):g}, "
|
||||
f"target_radius={target_radius:g}, "
|
||||
f"delta_radius={float(plan['delta_radius']):g}, "
|
||||
f"support_faces={plan.get('feature_existing_fillet_support_face_ids')}, "
|
||||
f"risk={plan['risk']}."
|
||||
f"risk={plan['risk']}. {result_check}"
|
||||
)
|
||||
|
||||
def fillet_edge(self, edge_id: int, radius: float) -> str:
|
||||
@@ -10231,16 +10585,24 @@ class OperationMixin:
|
||||
if part is None:
|
||||
raise ValueError(f"Unknown part id {part_id}")
|
||||
|
||||
maker = BRepFilletAPI_MakeFillet(part.shape)
|
||||
maker.Add(radius, topods.Edge(self.edges[edge_id]))
|
||||
result = _finalize_builder_result(maker, "edge fillet")
|
||||
part.shape = result
|
||||
self.refresh_topology()
|
||||
old_part_shape = part.shape
|
||||
before_stats = self.part_topology_stats(part_id)
|
||||
try:
|
||||
maker = BRepFilletAPI_MakeFillet(part.shape)
|
||||
maker.Add(radius, topods.Edge(self.edges[edge_id]))
|
||||
result = _finalize_builder_result(maker, "edge fillet")
|
||||
part.shape = result
|
||||
self.refresh_topology()
|
||||
result_check = self._edge_blend_result_summary_or_raise(plan, before_stats, mode="fillet")
|
||||
except Exception:
|
||||
part.shape = old_part_shape
|
||||
self.refresh_topology()
|
||||
raise
|
||||
return (
|
||||
f"Edge fillet completed: edge {edge_id}, radius={radius:g}, "
|
||||
f"edge_length={float(plan['edge_length']):g}, "
|
||||
f"radius_to_length_ratio={float(plan['radius_to_length_ratio']):g}, "
|
||||
f"risk={plan['risk']}."
|
||||
f"risk={plan['risk']}. {result_check}"
|
||||
)
|
||||
|
||||
def chamfer_edge(self, edge_id: int, distance: float) -> str:
|
||||
@@ -10253,16 +10615,24 @@ class OperationMixin:
|
||||
if part is None:
|
||||
raise ValueError(f"Unknown part id {part_id}")
|
||||
|
||||
maker = BRepFilletAPI_MakeChamfer(part.shape)
|
||||
maker.Add(distance, topods.Edge(self.edges[edge_id]))
|
||||
result = _finalize_builder_result(maker, "edge chamfer")
|
||||
part.shape = result
|
||||
self.refresh_topology()
|
||||
old_part_shape = part.shape
|
||||
before_stats = self.part_topology_stats(part_id)
|
||||
try:
|
||||
maker = BRepFilletAPI_MakeChamfer(part.shape)
|
||||
maker.Add(distance, topods.Edge(self.edges[edge_id]))
|
||||
result = _finalize_builder_result(maker, "edge chamfer")
|
||||
part.shape = result
|
||||
self.refresh_topology()
|
||||
result_check = self._edge_blend_result_summary_or_raise(plan, before_stats, mode="chamfer")
|
||||
except Exception:
|
||||
part.shape = old_part_shape
|
||||
self.refresh_topology()
|
||||
raise
|
||||
return (
|
||||
f"Edge chamfer completed: edge {edge_id}, distance={distance:g}, "
|
||||
f"edge_length={float(plan['edge_length']):g}, "
|
||||
f"distance_to_length_ratio={float(plan['distance_to_length_ratio']):g}, "
|
||||
f"risk={plan['risk']}."
|
||||
f"risk={plan['risk']}. {result_check}"
|
||||
)
|
||||
|
||||
def chamfer_edge_asymmetric(
|
||||
@@ -10282,19 +10652,29 @@ class OperationMixin:
|
||||
raise ValueError(f"Unknown part id {part_id}")
|
||||
resolved_reference_face_id = int(plan["reference_face_id"])
|
||||
|
||||
maker = BRepFilletAPI_MakeChamfer(part.shape)
|
||||
old_part_shape = part.shape
|
||||
before_stats = self.part_topology_stats(part_id)
|
||||
try:
|
||||
maker.Add(
|
||||
float(distance1),
|
||||
float(distance2),
|
||||
topods.Edge(self.edges[edge_id]),
|
||||
topods.Face(self.faces[resolved_reference_face_id]),
|
||||
)
|
||||
except TypeError as exc:
|
||||
raise RuntimeError("The current OCCT binding does not support asymmetric chamfer Add(D1, D2, Edge, Face).") from exc
|
||||
result = _finalize_builder_result(maker, "asymmetric edge chamfer")
|
||||
part.shape = result
|
||||
self.refresh_topology()
|
||||
maker = BRepFilletAPI_MakeChamfer(part.shape)
|
||||
try:
|
||||
maker.Add(
|
||||
float(distance1),
|
||||
float(distance2),
|
||||
topods.Edge(self.edges[edge_id]),
|
||||
topods.Face(self.faces[resolved_reference_face_id]),
|
||||
)
|
||||
except TypeError as exc:
|
||||
raise RuntimeError(
|
||||
"The current OCCT binding does not support asymmetric chamfer Add(D1, D2, Edge, Face)."
|
||||
) from exc
|
||||
result = _finalize_builder_result(maker, "asymmetric edge chamfer")
|
||||
part.shape = result
|
||||
self.refresh_topology()
|
||||
result_check = self._edge_blend_result_summary_or_raise(plan, before_stats, mode="asymmetric_chamfer")
|
||||
except Exception:
|
||||
part.shape = old_part_shape
|
||||
self.refresh_topology()
|
||||
raise
|
||||
return (
|
||||
f"Asymmetric Edge chamfer completed: edge {edge_id}, "
|
||||
f"distance1={float(distance1):g}, distance2={float(distance2):g}, "
|
||||
@@ -10302,7 +10682,7 @@ class OperationMixin:
|
||||
f"edge_length={float(plan['edge_length']):g}, "
|
||||
f"distance1_to_length_ratio={float(plan['distance1_to_length_ratio']):g}, "
|
||||
f"distance2_to_length_ratio={float(plan['distance2_to_length_ratio']):g}, "
|
||||
f"risk={plan['risk']}."
|
||||
f"risk={plan['risk']}. {result_check}"
|
||||
)
|
||||
|
||||
def chamfer_edge_distance_angle(
|
||||
@@ -10322,28 +10702,40 @@ class OperationMixin:
|
||||
raise ValueError(f"Unknown part id {part_id}")
|
||||
resolved_reference_face_id = int(plan["reference_face_id"])
|
||||
|
||||
maker = BRepFilletAPI_MakeChamfer(part.shape)
|
||||
old_part_shape = part.shape
|
||||
before_stats = self.part_topology_stats(part_id)
|
||||
try:
|
||||
maker.AddDA(
|
||||
float(distance),
|
||||
float(plan["target_angle_radians"]),
|
||||
topods.Edge(self.edges[edge_id]),
|
||||
topods.Face(self.faces[resolved_reference_face_id]),
|
||||
)
|
||||
except AttributeError as exc:
|
||||
raise RuntimeError("The current OCCT binding does not support distance-angle chamfer AddDA(D, Angle, Edge, Face).") from exc
|
||||
except TypeError as exc:
|
||||
raise RuntimeError("The current OCCT binding rejected distance-angle chamfer AddDA(D, Angle, Edge, Face).") from exc
|
||||
result = _finalize_builder_result(maker, "distance-angle edge chamfer")
|
||||
part.shape = result
|
||||
self.refresh_topology()
|
||||
maker = BRepFilletAPI_MakeChamfer(part.shape)
|
||||
try:
|
||||
maker.AddDA(
|
||||
float(distance),
|
||||
float(plan["target_angle_radians"]),
|
||||
topods.Edge(self.edges[edge_id]),
|
||||
topods.Face(self.faces[resolved_reference_face_id]),
|
||||
)
|
||||
except AttributeError as exc:
|
||||
raise RuntimeError(
|
||||
"The current OCCT binding does not support distance-angle chamfer AddDA(D, Angle, Edge, Face)."
|
||||
) from exc
|
||||
except TypeError as exc:
|
||||
raise RuntimeError(
|
||||
"The current OCCT binding rejected distance-angle chamfer AddDA(D, Angle, Edge, Face)."
|
||||
) from exc
|
||||
result = _finalize_builder_result(maker, "distance-angle edge chamfer")
|
||||
part.shape = result
|
||||
self.refresh_topology()
|
||||
result_check = self._edge_blend_result_summary_or_raise(plan, before_stats, mode="distance_angle_chamfer")
|
||||
except Exception:
|
||||
part.shape = old_part_shape
|
||||
self.refresh_topology()
|
||||
raise
|
||||
return (
|
||||
f"Distance-angle Edge chamfer completed: edge {edge_id}, "
|
||||
f"distance={float(distance):g}, angle_degrees={float(angle_degrees):g}, "
|
||||
f"reference_face={resolved_reference_face_id}, "
|
||||
f"edge_length={float(plan['edge_length']):g}, "
|
||||
f"distance_to_length_ratio={float(plan['distance_to_length_ratio']):g}, "
|
||||
f"risk={plan['risk']}."
|
||||
f"risk={plan['risk']}. {result_check}"
|
||||
)
|
||||
|
||||
def enlarge_cylindrical_hole(self, face_id: int, new_diameter: float) -> str:
|
||||
@@ -11892,41 +12284,62 @@ class OperationMixin:
|
||||
if part is None:
|
||||
raise ValueError(f"Unknown part id {part_id}")
|
||||
|
||||
start = gp_Pnt(*plan["boss_tool_start_point"])
|
||||
direction = gp_Dir(*plan["boss_tool_axis_direction"])
|
||||
axis = gp_Ax2(start, direction)
|
||||
height = float(plan["boss_tool_height"])
|
||||
if plan["resize_mode"] == "enlarge":
|
||||
tool = BRepPrimAPI_MakeCylinder(axis, float(plan["boss_tool_radius"]), height).Shape()
|
||||
op = BRepAlgoAPI_Fuse(part.shape, tool)
|
||||
result = _finalize_boolean_result(op, "cylindrical boss fuse", use_glue=False)
|
||||
action = "enlarged by bounded fuse"
|
||||
else:
|
||||
removal = BRepPrimAPI_MakeCylinder(axis, float(plan["boss_tool_outer_radius"]), height).Shape()
|
||||
replacement = BRepPrimAPI_MakeCylinder(axis, float(plan["boss_tool_inner_radius"]), height).Shape()
|
||||
remove_op = BRepAlgoAPI_Cut(part.shape, removal)
|
||||
removed = _finalize_boolean_result(remove_op, "cylindrical boss shrink remove envelope", use_glue=False)
|
||||
if _topology_shape_count(removed, TopAbs_FACE) == 0:
|
||||
exact_start = gp_Pnt(*plan["boss_tool_exact_start_point"])
|
||||
exact_axis = gp_Ax2(exact_start, direction)
|
||||
exact_replacement = BRepPrimAPI_MakeCylinder(
|
||||
exact_axis,
|
||||
float(plan["boss_tool_inner_radius"]),
|
||||
float(plan["boss_tool_exact_height"]),
|
||||
).Shape()
|
||||
result = _ensure_valid_or_repaired_shape(exact_replacement, "cylindrical boss shrink replacement")
|
||||
old_part_shape = part.shape
|
||||
verification: dict[str, object] = {}
|
||||
try:
|
||||
start = gp_Pnt(*plan["boss_tool_start_point"])
|
||||
direction = gp_Dir(*plan["boss_tool_axis_direction"])
|
||||
axis = gp_Ax2(start, direction)
|
||||
height = float(plan["boss_tool_height"])
|
||||
if plan["resize_mode"] == "enlarge":
|
||||
tool = BRepPrimAPI_MakeCylinder(axis, float(plan["boss_tool_radius"]), height).Shape()
|
||||
op = BRepAlgoAPI_Fuse(part.shape, tool)
|
||||
result = _finalize_boolean_result(op, "cylindrical boss fuse", use_glue=False)
|
||||
action = "enlarged by bounded fuse"
|
||||
else:
|
||||
fuse_op = BRepAlgoAPI_Fuse(removed, replacement)
|
||||
result = _finalize_boolean_result(fuse_op, "cylindrical boss shrink rebuild", use_glue=False)
|
||||
action = "shrunk by removing old envelope and fusing target cylinder"
|
||||
removal = BRepPrimAPI_MakeCylinder(axis, float(plan["boss_tool_outer_radius"]), height).Shape()
|
||||
replacement = BRepPrimAPI_MakeCylinder(axis, float(plan["boss_tool_inner_radius"]), height).Shape()
|
||||
remove_op = BRepAlgoAPI_Cut(part.shape, removal)
|
||||
removed = _finalize_boolean_result(remove_op, "cylindrical boss shrink remove envelope", use_glue=False)
|
||||
if _topology_shape_count(removed, TopAbs_FACE) == 0:
|
||||
exact_start = gp_Pnt(*plan["boss_tool_exact_start_point"])
|
||||
exact_axis = gp_Ax2(exact_start, direction)
|
||||
exact_replacement = BRepPrimAPI_MakeCylinder(
|
||||
exact_axis,
|
||||
float(plan["boss_tool_inner_radius"]),
|
||||
float(plan["boss_tool_exact_height"]),
|
||||
).Shape()
|
||||
result = _ensure_valid_or_repaired_shape(exact_replacement, "cylindrical boss shrink replacement")
|
||||
else:
|
||||
fuse_op = BRepAlgoAPI_Fuse(removed, replacement)
|
||||
result = _finalize_boolean_result(fuse_op, "cylindrical boss shrink rebuild", use_glue=False)
|
||||
action = "shrunk by removing old envelope and fusing target cylinder"
|
||||
|
||||
part.shape = result
|
||||
self.refresh_topology()
|
||||
|
||||
verification_plan = dict(plan)
|
||||
verification_plan["cutter_axis_point"] = plan["boss_tool_axis_point"]
|
||||
verification_plan["cutter_axis_direction"] = plan["boss_tool_axis_direction"]
|
||||
verification = self._verify_cylindrical_resize_result(verification_plan, part_id)
|
||||
if not verification["matched"]:
|
||||
detail = str(verification.get("detail", "cylindrical boss resize result verification failed"))
|
||||
raise RuntimeError(
|
||||
"Cylindrical boss resize returned a result, but no target-diameter boss with "
|
||||
f"valid first-level topology was found; rolled back to the previous model.{detail}"
|
||||
)
|
||||
except Exception:
|
||||
part.shape = old_part_shape
|
||||
self.refresh_topology()
|
||||
raise
|
||||
|
||||
part.shape = result
|
||||
self.refresh_topology()
|
||||
return (
|
||||
f"Cylindrical boss resize completed: diameter {float(plan['current_diameter']):g} -> {new_diameter:g}, "
|
||||
f"mode={plan['resize_mode']}, action={action}, risk={plan['risk']}, "
|
||||
f"feature={plan['feature_guess']}, tool={plan['boss_tool_strategy']}, "
|
||||
f"height={float(plan['boss_tool_height']):g}."
|
||||
f"height={float(plan['boss_tool_height']):g}, "
|
||||
f"verified_face={verification.get('face_id', '')}, "
|
||||
f"first_level_topology_matched={verification.get('first_level_topology_matched', '')}."
|
||||
)
|
||||
|
||||
def move_cylindrical_boss_axis(
|
||||
@@ -12011,7 +12424,8 @@ class OperationMixin:
|
||||
f"diameter={float(plan['target_diameter']):g}, "
|
||||
f"height={float(plan['boss_tool_height']):g}, "
|
||||
f"risk={plan['risk']}, action={action}, "
|
||||
f"verified_face={verification.get('face_id', '')}."
|
||||
f"verified_face={verification.get('face_id', '')}, "
|
||||
f"first_level_topology_matched={verification.get('first_level_topology_matched', '')}."
|
||||
)
|
||||
|
||||
def suppress_cylindrical_hole(self, face_id: int) -> str:
|
||||
|
||||
Reference in New Issue
Block a user