feat: 完善特征编辑和选择稳定性

This commit is contained in:
2026-07-29 15:43:28 +08:00
parent 720c2dc970
commit c937e13d43
13 changed files with 8294 additions and 290 deletions
+297
View File
@@ -262,3 +262,300 @@ class TransformMixin:
f"risk={plan['risk']}."
)
def scale_part_plan(self, part_id: int, target_diagonal: float) -> dict[str, object]:
part = self.part_by_id(part_id)
if part is None:
raise ValueError(f"Unknown part id {part_id}")
return self._scale_shape_plan(
target_kind="part",
part_id=part.id,
solid_id=-1,
shape=part.shape,
target_diagonal=target_diagonal,
part_solid_count=len(_explore(part.shape, TopAbs_SOLID)),
)
def scale_solid_plan(self, solid_id: int, target_diagonal: float) -> dict[str, object]:
if solid_id < 0 or solid_id >= len(self.solids):
raise ValueError(f"Unknown solid id {solid_id}")
part_id, solid = self.solids[solid_id]
part = self.part_by_id(part_id)
part_solid_count = len(_explore(part.shape, TopAbs_SOLID)) if part is not None else 0
plan = self._scale_shape_plan(
target_kind="solid",
part_id=part_id,
solid_id=solid_id,
shape=solid,
target_diagonal=target_diagonal,
part_solid_count=part_solid_count,
)
if part_solid_count <= 1 and plan["status"] != "blocked":
plan["warnings"] = _join_nonempty(
plan.get("warnings", ""),
"当前零件只有一个Solid,缩放Solid实际会缩放整个零件 shape。",
)
if plan["risk"] == "low":
plan["risk"] = "medium"
plan["status"] = "caution"
plan["message"] = _join_nonempty(plan.get("message", ""), plan.get("warnings", ""))
return plan
def scale_part_axis_plan(self, part_id: int, axis: str, target_size: float) -> dict[str, object]:
part = self.part_by_id(part_id)
if part is None:
raise ValueError(f"Unknown part id {part_id}")
return self._axis_scale_shape_plan(
target_kind="part",
part_id=part.id,
solid_id=-1,
shape=part.shape,
axis=axis,
target_size=target_size,
part_solid_count=len(_explore(part.shape, TopAbs_SOLID)),
)
def scale_solid_axis_plan(self, solid_id: int, axis: str, target_size: float) -> dict[str, object]:
if solid_id < 0 or solid_id >= len(self.solids):
raise ValueError(f"Unknown solid id {solid_id}")
part_id, solid = self.solids[solid_id]
part = self.part_by_id(part_id)
part_solid_count = len(_explore(part.shape, TopAbs_SOLID)) if part is not None else 0
plan = self._axis_scale_shape_plan(
target_kind="solid",
part_id=part_id,
solid_id=solid_id,
shape=solid,
axis=axis,
target_size=target_size,
part_solid_count=part_solid_count,
)
if part_solid_count <= 1 and plan["status"] != "blocked":
plan["warnings"] = _join_nonempty(
plan.get("warnings", ""),
"当前零件只有一个Solid,按轴缩放Solid实际会缩放整个零件shape。",
)
if plan["risk"] == "low":
plan["risk"] = "medium"
plan["status"] = "caution"
plan["message"] = _join_nonempty(plan.get("message", ""), plan.get("warnings", ""))
return plan
def _scale_shape_plan(
self,
*,
target_kind: str,
part_id: int,
solid_id: int,
shape: TopoDS_Shape,
target_diagonal: float,
part_solid_count: int,
) -> dict[str, object]:
blockers: list[str] = []
warnings: list[str] = []
try:
target_diagonal = float(target_diagonal)
except (TypeError, ValueError):
target_diagonal = 0.0
blockers.append("目标包围盒对角线必须是数字。")
current_diagonal = _shape_diagonal(shape)
center = _shape_center(shape)
if current_diagonal <= 1e-9:
blockers.append("当前对象包围盒对角线无效,不能缩放。")
if target_diagonal <= 1e-9:
blockers.append("目标包围盒对角线必须大于 0。")
scale = target_diagonal / max(current_diagonal, 1e-9)
delta_ratio = abs(target_diagonal - current_diagonal) / max(current_diagonal, 1e-9)
risk = "low"
status = "ready"
if abs(target_diagonal - current_diagonal) <= max(current_diagonal * 1e-6, 1e-6):
blockers.append("目标包围盒对角线与当前值几乎相同,不需要缩放。")
elif delta_ratio > 1.0:
risk = "high"
status = "caution"
warnings.append("目标尺寸变化超过当前包围盒对角线的 100%,请确认单位和目标。")
elif delta_ratio > 0.35:
risk = "medium"
status = "caution"
warnings.append("目标尺寸变化超过当前包围盒对角线的 35%,请确认缩放结果。")
if blockers:
risk = "blocked"
status = "blocked"
return {
"status": status,
"risk": risk,
"message": _join_nonempty(" ".join(blockers), " ".join(warnings)) or "可以按目标包围盒对角线等比缩放当前对象。",
"warnings": "".join(warnings),
"blockers": "".join(blockers),
"target_kind": target_kind,
"part_id": part_id,
"solid_id": solid_id,
"part_solid_count": part_solid_count,
"current_bbox_diagonal": current_diagonal,
"target_bbox_diagonal": target_diagonal,
"scale_factor": scale,
"scale_delta_ratio": delta_ratio,
"scale_center": center,
"resize_strategy": "uniform-scale-selected-shape-to-bbox-diagonal",
}
def _axis_scale_shape_plan(
self,
*,
target_kind: str,
part_id: int,
solid_id: int,
shape: TopoDS_Shape,
axis: str,
target_size: float,
part_solid_count: int,
) -> dict[str, object]:
axis = str(axis or "X").strip().upper()
axis_index = {"X": 0, "Y": 1, "Z": 2}.get(axis)
blockers: list[str] = []
warnings: list[str] = [
"按单轴尺寸缩放是 B-Rep 仿射编辑,可能把解析几何转换成 B-spline 或影响同一对象上的其它特征。"
]
try:
target_size = float(target_size)
except (TypeError, ValueError):
target_size = 0.0
blockers.append("目标轴向尺寸必须是数字。")
bounds = _shape_bounds_info(shape)
bbox_size = tuple(bounds.get("bbox_size", (0.0, 0.0, 0.0)))
current_size = float(bbox_size[axis_index]) if axis_index is not None else 0.0
center = _shape_center(shape)
if axis_index is None:
blockers.append("缩放轴必须是 X、Y 或 Z。")
if current_size <= 1e-9:
blockers.append("当前对象在该轴向的包围盒尺寸无效,不能按轴缩放。")
if target_size <= 1e-9:
blockers.append("目标轴向尺寸必须大于 0。")
scale = target_size / max(current_size, 1e-9)
delta_ratio = abs(target_size - current_size) / max(current_size, 1e-9)
risk = "medium"
status = "caution"
if abs(target_size - current_size) <= max(current_size * 1e-6, 1e-6):
blockers.append("目标轴向尺寸与当前值几乎相同,不需要缩放。")
elif delta_ratio > 1.0:
risk = "high"
warnings.append("目标轴向尺寸变化超过当前尺寸的 100%,请确认单位和目标。")
elif delta_ratio <= 0.2:
risk = "medium"
if blockers:
risk = "blocked"
status = "blocked"
return {
"status": status,
"risk": risk,
"message": _join_nonempty(" ".join(blockers), " ".join(warnings)) or "可以按目标轴向尺寸缩放当前对象。",
"warnings": "".join(warnings),
"blockers": "".join(blockers),
"target_kind": target_kind,
"part_id": part_id,
"solid_id": solid_id,
"part_solid_count": part_solid_count,
"scale_axis": axis,
"current_axis_size": current_size,
"target_axis_size": target_size,
"axis_scale_factor": scale,
"axis_scale_delta_ratio": delta_ratio,
"scale_center": center,
"bbox_size": bbox_size,
"resize_strategy": "axis-scale-selected-shape-to-bbox-size",
}
def scale_part(self, part_id: int, target_diagonal: float) -> str:
plan = self.scale_part_plan(part_id, target_diagonal)
if plan["status"] == "blocked":
raise ValueError(str(plan["message"]))
part = self.part_by_id(part_id)
if part is None:
raise ValueError(f"Unknown part id {part_id}")
part.shape = _scaled_shape(part.shape, float(plan["scale_factor"]), plan["scale_center"])
_ensure_valid_shape(part.shape)
self.refresh_topology()
return (
f"Part scaled: part {part_id}, "
f"bbox_diagonal={float(plan['current_bbox_diagonal']):g}->{float(plan['target_bbox_diagonal']):g}, "
f"scale={float(plan['scale_factor']):g}, risk={plan['risk']}."
)
def scale_part_axis(self, part_id: int, axis: str, target_size: float) -> str:
plan = self.scale_part_axis_plan(part_id, axis, target_size)
if plan["status"] == "blocked":
raise ValueError(str(plan["message"]))
part = self.part_by_id(part_id)
if part is None:
raise ValueError(f"Unknown part id {part_id}")
part.shape = _axis_scaled_shape(part.shape, str(plan["scale_axis"]), float(plan["axis_scale_factor"]), plan["scale_center"])
_ensure_valid_shape(part.shape)
self.refresh_topology()
return (
f"Part axis-scaled: part {part_id}, axis={plan['scale_axis']}, "
f"size={float(plan['current_axis_size']):g}->{float(plan['target_axis_size']):g}, "
f"scale={float(plan['axis_scale_factor']):g}, risk={plan['risk']}."
)
def scale_solid(self, solid_id: int, target_diagonal: float) -> str:
plan = self.scale_solid_plan(solid_id, target_diagonal)
if plan["status"] == "blocked":
raise ValueError(str(plan["message"]))
part_id, solid = self.solids[solid_id]
part = self.part_by_id(part_id)
if part is None:
raise ValueError(f"Unknown part id {part_id}")
part_solids = _explore(part.shape, TopAbs_SOLID)
if len(part_solids) <= 1:
part.shape = _scaled_shape(part.shape, float(plan["scale_factor"]), plan["scale_center"])
else:
scaled = _scaled_shape(solid, float(plan["scale_factor"]), plan["scale_center"])
replaced = False
shapes: list[TopoDS_Shape] = []
for item in part_solids:
if not replaced and _same_shape(item, solid):
shapes.append(scaled)
replaced = True
else:
shapes.append(item)
if not replaced:
raise RuntimeError(f"Could not locate solid {solid_id} inside part {part_id}.")
part.shape = _compound_from_shapes(shapes)
_ensure_valid_shape(part.shape)
self.refresh_topology()
return (
f"Solid scaled: solid {solid_id}, part {part_id}, "
f"bbox_diagonal={float(plan['current_bbox_diagonal']):g}->{float(plan['target_bbox_diagonal']):g}, "
f"scale={float(plan['scale_factor']):g}, risk={plan['risk']}."
)
def scale_solid_axis(self, solid_id: int, axis: str, target_size: float) -> str:
plan = self.scale_solid_axis_plan(solid_id, axis, target_size)
if plan["status"] == "blocked":
raise ValueError(str(plan["message"]))
part_id, solid = self.solids[solid_id]
part = self.part_by_id(part_id)
if part is None:
raise ValueError(f"Unknown part id {part_id}")
part_solids = _explore(part.shape, TopAbs_SOLID)
if len(part_solids) <= 1:
part.shape = _axis_scaled_shape(part.shape, str(plan["scale_axis"]), float(plan["axis_scale_factor"]), plan["scale_center"])
else:
scaled = _axis_scaled_shape(solid, str(plan["scale_axis"]), float(plan["axis_scale_factor"]), plan["scale_center"])
replaced = False
shapes: list[TopoDS_Shape] = []
for item in part_solids:
if not replaced and _same_shape(item, solid):
shapes.append(scaled)
replaced = True
else:
shapes.append(item)
if not replaced:
raise RuntimeError(f"Could not locate solid {solid_id} inside part {part_id}.")
part.shape = _compound_from_shapes(shapes)
_ensure_valid_shape(part.shape)
self.refresh_topology()
return (
f"Solid axis-scaled: solid {solid_id}, part {part_id}, axis={plan['scale_axis']}, "
f"size={float(plan['current_axis_size']):g}->{float(plan['target_axis_size']):g}, "
f"scale={float(plan['axis_scale_factor']):g}, risk={plan['risk']}."
)