566 lines
25 KiB
Python
566 lines
25 KiB
Python
from __future__ import annotations
|
||
|
||
import math
|
||
from pathlib import Path
|
||
from typing import Callable, Iterable
|
||
|
||
from OCC.Core.BRep import BRep_Tool
|
||
from OCC.Core.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface
|
||
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Defeaturing, BRepAlgoAPI_Fuse
|
||
from OCC.Core.BRepBndLib import brepbndlib
|
||
from OCC.Core.BOPAlgo import BOPAlgo_GlueFull
|
||
from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform
|
||
from OCC.Core.BRepCheck import BRepCheck_Analyzer
|
||
from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier
|
||
from OCC.Core.BRepFilletAPI import BRepFilletAPI_MakeChamfer, BRepFilletAPI_MakeFillet
|
||
from OCC.Core.BRepGProp import brepgprop
|
||
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh
|
||
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeCylinder, BRepPrimAPI_MakePrism
|
||
from OCC.Core.Bnd import Bnd_Box
|
||
from OCC.Core.GeomAbs import (
|
||
GeomAbs_BSplineCurve,
|
||
GeomAbs_BSplineSurface,
|
||
GeomAbs_BezierCurve,
|
||
GeomAbs_BezierSurface,
|
||
GeomAbs_Circle,
|
||
GeomAbs_Cone,
|
||
GeomAbs_Cylinder,
|
||
GeomAbs_Ellipse,
|
||
GeomAbs_Hyperbola,
|
||
GeomAbs_Line,
|
||
GeomAbs_OffsetSurface,
|
||
GeomAbs_OtherCurve,
|
||
GeomAbs_OtherSurface,
|
||
GeomAbs_Parabola,
|
||
GeomAbs_Plane,
|
||
GeomAbs_Sphere,
|
||
GeomAbs_SurfaceOfExtrusion,
|
||
GeomAbs_SurfaceOfRevolution,
|
||
GeomAbs_Torus,
|
||
)
|
||
from OCC.Core.GProp import GProp_GProps
|
||
from OCC.Core.ShapeFix import ShapeFix_Shape
|
||
from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain
|
||
from OCC.Core.TopAbs import (
|
||
TopAbs_EDGE,
|
||
TopAbs_EXTERNAL,
|
||
TopAbs_FACE,
|
||
TopAbs_FORWARD,
|
||
TopAbs_IN,
|
||
TopAbs_INTERNAL,
|
||
TopAbs_OUT,
|
||
TopAbs_REVERSED,
|
||
TopAbs_SOLID,
|
||
)
|
||
from OCC.Core.TopExp import TopExp_Explorer, topexp
|
||
from OCC.Core.TopLoc import TopLoc_Location
|
||
from OCC.Core.TopoDS import TopoDS_Compound, TopoDS_Shape, topods
|
||
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape, TopTools_IndexedMapOfShape
|
||
from OCC.Core.gp import gp_Ax1, gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf, gp_Vec
|
||
from OCC.Extend.TopologyUtils import TopologyExplorer, discretize_edge
|
||
|
||
from .constants import CURVE_TYPES, SNAPSHOT_FACE_LOGICAL_IDS_KEY, SURFACE_TYPES
|
||
from .geometry_utils import * # noqa: F403
|
||
|
||
|
||
class TransformMixin:
|
||
def translate_part_plan(self, part_id: int, vector: tuple[float, float, float]) -> dict[str, object]:
|
||
part = self.part_by_id(part_id)
|
||
if part is None:
|
||
raise ValueError(f"Unknown part id {part_id}")
|
||
readiness = _translation_readiness(vector, part.shape)
|
||
return {
|
||
"status": readiness["translate_status"],
|
||
"risk": readiness["translate_risk"],
|
||
"message": readiness["translate_note"],
|
||
"warnings": readiness["translate_warnings"],
|
||
"blockers": readiness["translate_blockers"],
|
||
"target_kind": "part",
|
||
"part_id": part.id,
|
||
"name": part.name,
|
||
"translation_vector": vector,
|
||
"translation_distance": _vector_length(vector),
|
||
"bbox_diagonal": _shape_diagonal(part.shape),
|
||
}
|
||
|
||
def translate_solid_plan(self, solid_id: int, vector: tuple[float, float, 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]
|
||
readiness = _translation_readiness(vector, solid)
|
||
part = self.part_by_id(part_id)
|
||
part_solid_count = len(_explore(part.shape, TopAbs_SOLID)) if part is not None else 0
|
||
warnings = readiness["translate_warnings"]
|
||
risk = readiness["translate_risk"]
|
||
status = readiness["translate_status"]
|
||
if part_solid_count <= 1 and status != "blocked":
|
||
warnings = _join_nonempty(warnings, "当前零件只有一个Solid,平移Solid实际会移动整个零件 shape。")
|
||
if risk == "low":
|
||
risk = "medium"
|
||
status = "caution"
|
||
return {
|
||
"status": status,
|
||
"risk": risk,
|
||
"message": _join_nonempty(readiness["translate_note"], warnings),
|
||
"warnings": warnings,
|
||
"blockers": readiness["translate_blockers"],
|
||
"target_kind": "solid",
|
||
"part_id": part_id,
|
||
"solid_id": solid_id,
|
||
"part_solid_count": part_solid_count,
|
||
"translation_vector": vector,
|
||
"translation_distance": _vector_length(vector),
|
||
"bbox_diagonal": _shape_diagonal(solid),
|
||
}
|
||
|
||
def translate_part(self, part_id: int, vector: tuple[float, float, float]) -> str:
|
||
plan = self.translate_part_plan(part_id, vector)
|
||
if plan["status"] == "blocked":
|
||
raise ValueError(str(plan["message"]))
|
||
part = self.part_by_id(part_id)
|
||
if part is None:
|
||
raise ValueError(f"未知零件 ID {part_id}")
|
||
previous_logical_ids = tuple(getattr(self, "face_logical_ids", ()))
|
||
part.shape = _translated_shape_by_vector(part.shape, vector)
|
||
_ensure_valid_shape(part.shape)
|
||
self.refresh_topology()
|
||
self._restore_face_logical_ids_if_count_matches(previous_logical_ids)
|
||
return (
|
||
f"零件已平移: 零件 {part_id}, vector={_format_tuple(vector)}, "
|
||
f"distance={float(plan['translation_distance']):g}, risk={plan['risk']}."
|
||
)
|
||
|
||
def translate_solid(self, solid_id: int, vector: tuple[float, float, float]) -> str:
|
||
plan = self.translate_solid_plan(solid_id, vector)
|
||
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}")
|
||
|
||
previous_logical_ids = tuple(getattr(self, "face_logical_ids", ()))
|
||
part_solids = _explore(part.shape, TopAbs_SOLID)
|
||
if len(part_solids) <= 1:
|
||
part.shape = _translated_shape_by_vector(part.shape, vector)
|
||
else:
|
||
translated = _translated_shape_by_vector(solid, vector)
|
||
replaced = False
|
||
shapes: list[TopoDS_Shape] = []
|
||
for item in part_solids:
|
||
if not replaced and _same_shape(item, solid):
|
||
shapes.append(translated)
|
||
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()
|
||
self._restore_face_logical_ids_if_count_matches(previous_logical_ids)
|
||
return (
|
||
f"Solid translated: solid {solid_id}, part {part_id}, vector={_format_tuple(vector)}, "
|
||
f"distance={float(plan['translation_distance']):g}, risk={plan['risk']}."
|
||
)
|
||
|
||
def rotate_part_plan(self, part_id: int, axis: str, angle_degrees: float) -> dict[str, object]:
|
||
part = self.part_by_id(part_id)
|
||
if part is None:
|
||
raise ValueError(f"Unknown part id {part_id}")
|
||
readiness = _rotation_readiness(axis, angle_degrees)
|
||
return {
|
||
"status": readiness["rotate_status"],
|
||
"risk": readiness["rotate_risk"],
|
||
"message": readiness["rotate_note"],
|
||
"warnings": readiness["rotate_warnings"],
|
||
"blockers": readiness["rotate_blockers"],
|
||
"target_kind": "part",
|
||
"part_id": part.id,
|
||
"name": part.name,
|
||
"rotation_axis": axis.upper(),
|
||
"rotation_angle_degrees": angle_degrees,
|
||
"rotation_center": _shape_center(part.shape),
|
||
"bbox_diagonal": _shape_diagonal(part.shape),
|
||
}
|
||
|
||
def rotate_solid_plan(self, solid_id: int, axis: str, angle_degrees: 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]
|
||
readiness = _rotation_readiness(axis, angle_degrees)
|
||
part = self.part_by_id(part_id)
|
||
part_solid_count = len(_explore(part.shape, TopAbs_SOLID)) if part is not None else 0
|
||
warnings = readiness["rotate_warnings"]
|
||
risk = readiness["rotate_risk"]
|
||
status = readiness["rotate_status"]
|
||
if part_solid_count <= 1 and status != "blocked":
|
||
warnings = _join_nonempty(warnings, "当前零件只有一个Solid,旋转Solid实际会旋转整个零件 shape。")
|
||
if risk == "low":
|
||
risk = "medium"
|
||
status = "caution"
|
||
return {
|
||
"status": status,
|
||
"risk": risk,
|
||
"message": _join_nonempty(readiness["rotate_note"], warnings),
|
||
"warnings": warnings,
|
||
"blockers": readiness["rotate_blockers"],
|
||
"target_kind": "solid",
|
||
"part_id": part_id,
|
||
"solid_id": solid_id,
|
||
"part_solid_count": part_solid_count,
|
||
"rotation_axis": axis.upper(),
|
||
"rotation_angle_degrees": angle_degrees,
|
||
"rotation_center": _shape_center(solid),
|
||
"bbox_diagonal": _shape_diagonal(solid),
|
||
}
|
||
|
||
def rotate_part(self, part_id: int, axis: str, angle_degrees: float) -> str:
|
||
plan = self.rotate_part_plan(part_id, axis, angle_degrees)
|
||
if plan["status"] == "blocked":
|
||
raise ValueError(str(plan["message"]))
|
||
part = self.part_by_id(part_id)
|
||
if part is None:
|
||
raise ValueError(f"未知零件 ID {part_id}")
|
||
part.shape = _rotated_shape(part.shape, str(plan["rotation_axis"]), float(plan["rotation_angle_degrees"]), plan["rotation_center"])
|
||
_ensure_valid_shape(part.shape)
|
||
self.refresh_topology()
|
||
return (
|
||
f"零件已旋转: 零件 {part_id}, axis={plan['rotation_axis']}, "
|
||
f"angle={float(plan['rotation_angle_degrees']):g}, center={_format_tuple(plan['rotation_center'])}, "
|
||
f"risk={plan['risk']}."
|
||
)
|
||
|
||
def rotate_solid(self, solid_id: int, axis: str, angle_degrees: float) -> str:
|
||
plan = self.rotate_solid_plan(solid_id, axis, angle_degrees)
|
||
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 = _rotated_shape(part.shape, str(plan["rotation_axis"]), float(plan["rotation_angle_degrees"]), plan["rotation_center"])
|
||
else:
|
||
rotated = _rotated_shape(solid, str(plan["rotation_axis"]), float(plan["rotation_angle_degrees"]), plan["rotation_center"])
|
||
replaced = False
|
||
shapes: list[TopoDS_Shape] = []
|
||
for item in part_solids:
|
||
if not replaced and _same_shape(item, solid):
|
||
shapes.append(rotated)
|
||
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 rotated: solid {solid_id}, part {part_id}, axis={plan['rotation_axis']}, "
|
||
f"angle={float(plan['rotation_angle_degrees']):g}, center={_format_tuple(plan['rotation_center'])}, "
|
||
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']}."
|
||
)
|