feat: 完善 Face 一级关系编辑和稳定性
This commit is contained in:
+702
-19
@@ -63,6 +63,7 @@ from OCC.Core.TopAbs import (
|
||||
TopAbs_OUT,
|
||||
TopAbs_REVERSED,
|
||||
TopAbs_SOLID,
|
||||
TopAbs_VERTEX,
|
||||
TopAbs_WIRE,
|
||||
)
|
||||
from OCC.Core.TopExp import TopExp_Explorer, topexp
|
||||
@@ -110,6 +111,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_edge_ids_cache: dict[int, list[int]] = {}
|
||||
self._edge_face_ids_cache: dict[int, list[int]] = {}
|
||||
self._same_domain_face_ids_cache: dict[int, list[int]] = {}
|
||||
self._face_first_level_topology_cache: dict[int, dict[str, object]] = {}
|
||||
self._cylindrical_first_level_topology_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
|
||||
@@ -191,6 +194,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_edge_ids_cache.clear()
|
||||
self._edge_face_ids_cache.clear()
|
||||
self._same_domain_face_ids_cache.clear()
|
||||
self._face_first_level_topology_cache.clear()
|
||||
self._cylindrical_first_level_topology_cache.clear()
|
||||
self._local_face_deform_readiness_cache.clear()
|
||||
self._edge_duplicate_key_ids_cache = None
|
||||
self._same_domain_internal_edge_ids_cache = None
|
||||
@@ -283,8 +288,24 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
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._local_face_deform_readiness_cache.clear()
|
||||
|
||||
def _restore_face_logical_ids_if_count_matches(self, logical_ids: Iterable[int]) -> bool:
|
||||
previous = tuple(int(item) for item in logical_ids)
|
||||
if len(previous) != len(self.faces):
|
||||
return False
|
||||
self.face_logical_ids = list(previous)
|
||||
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._local_face_deform_readiness_cache.clear()
|
||||
return True
|
||||
|
||||
def quick_face_info(self, face_id: int) -> dict[str, object]:
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
raise ValueError(f"Unknown face id {face_id}")
|
||||
@@ -323,16 +344,11 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
if surface_type == GeomAbs_Plane:
|
||||
plane = surf.Plane()
|
||||
direction = plane.Axis().Direction()
|
||||
push_pull_direction = self._plane_push_pull_direction(face_id, surf)
|
||||
info["plane_origin"] = _point_tuple(plane.Location())
|
||||
info["normal"] = _dir_tuple(direction)
|
||||
info["oriented_normal"] = _oriented_dir_tuple(direction, face)
|
||||
info["push_pull_outward_direction"] = push_pull_direction["outward_direction"]
|
||||
info["push_pull_inward_direction"] = push_pull_direction["inward_direction"]
|
||||
info["push_pull_plus_side"] = push_pull_direction["plus_side_state"]
|
||||
info["push_pull_minus_side"] = push_pull_direction["minus_side_state"]
|
||||
info["push_pull_confidence"] = push_pull_direction["confidence"]
|
||||
info["push_pull_note"] = push_pull_direction["note"]
|
||||
info["push_pull_confidence"] = "unchecked"
|
||||
info["push_pull_note"] = "快速选择阶段不判断材料内外方向;执行推拉时会重新计算。"
|
||||
info["push_pull_status"] = "candidate"
|
||||
info["feature_type"] = "可推拉平面候选"
|
||||
info["feature_source_face_id"] = face_id
|
||||
@@ -353,7 +369,6 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
radius = cyl.Radius()
|
||||
u_span = abs(surf.LastUParameter() - surf.FirstUParameter())
|
||||
swept_area = max(radius * max(u_span, 1e-9), 1e-9)
|
||||
classification = self._classify_cylindrical_face(face_id, surf, detailed=False)
|
||||
info["radius"] = radius
|
||||
info["diameter"] = radius * 2.0
|
||||
info["axis_point"] = _point_tuple(axis.Location())
|
||||
@@ -361,13 +376,13 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
info["angular_span"] = u_span
|
||||
info["is_full_cylinder"] = u_span >= math.tau * 0.98
|
||||
info["height_estimate"] = props.Mass() / swept_area
|
||||
info["feature_guess"] = classification["feature_guess"]
|
||||
info["confidence"] = classification["confidence"]
|
||||
info["material_toward_axis"] = classification["toward_axis"]
|
||||
info["material_away_axis"] = classification["away_axis"]
|
||||
info["material_vote_summary"] = classification["vote_summary"]
|
||||
info["material_sample_count"] = classification["sample_count"]
|
||||
info["note"] = classification["note"]
|
||||
info["feature_guess"] = "cylindrical face"
|
||||
info["confidence"] = "unchecked"
|
||||
info["material_toward_axis"] = "not sampled"
|
||||
info["material_away_axis"] = "not sampled"
|
||||
info["material_vote_summary"] = "quick selection skips material sampling"
|
||||
info["material_sample_count"] = 0
|
||||
info["note"] = "快速选择阶段不判断孔/槽/凸台;需要语义识别时切换特征探测级别。"
|
||||
info["feature_source_face_id"] = face_id
|
||||
info["feature_highlight_face_ids"] = (face_id,)
|
||||
if u_span < math.tau * 0.92:
|
||||
@@ -572,6 +587,112 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._feature_info_cache[face_id] = dict(result)
|
||||
return dict(result)
|
||||
|
||||
def associated_feature_infos(
|
||||
self,
|
||||
face_id: int,
|
||||
*,
|
||||
max_depth: int = 3,
|
||||
max_scan_faces: int = 72,
|
||||
max_features: int = 10,
|
||||
) -> list[dict[str, object]]:
|
||||
"""Detect editable feature candidates near the selected face.
|
||||
|
||||
STEP does not store a dependable CAD feature-history graph, so this
|
||||
uses a shallow shared-edge walk. It can cross small cap/support faces to
|
||||
reach a nearby hole, slot, boss, or analytic surface, but it avoids
|
||||
scanning an entire solid through large carrier planes.
|
||||
"""
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
raise ValueError(f"Unknown face id {face_id}")
|
||||
|
||||
source_info = 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}
|
||||
frontier: list[tuple[int, int]] = [(face_id, 0)]
|
||||
candidate_hops: dict[int, int] = {}
|
||||
|
||||
while frontier and len(visited) < max_scan_faces:
|
||||
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:
|
||||
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
|
||||
visited.add(neighbor_id)
|
||||
|
||||
expand = True
|
||||
if neighbor_id != face_id and depth >= 1:
|
||||
quick = self.quick_face_info(neighbor_id)
|
||||
neighbor_area = float(quick.get("area", 0.0) or 0.0)
|
||||
if str(quick.get("surface", "")) == "plane" and neighbor_area > source_area * 8.0:
|
||||
expand = False
|
||||
if expand:
|
||||
frontier.append((neighbor_id, depth + 1))
|
||||
|
||||
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 candidate_id in source_feature_faces:
|
||||
continue
|
||||
try:
|
||||
info = 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 not is_semantic:
|
||||
continue
|
||||
identity_face_ids = _int_values(info.get("feature_face_ids"))
|
||||
if info.get("prismatic_profile_status") == "candidate":
|
||||
identity_face_ids = (
|
||||
_int_values(info.get("prismatic_highlight_face_ids"))
|
||||
or _int_values(info.get("feature_highlight_face_ids"))
|
||||
)
|
||||
feature_faces = frozenset(identity_face_ids or [candidate_id])
|
||||
identity = (feature_type or feature_guess or surface, feature_faces)
|
||||
if identity in seen_features:
|
||||
continue
|
||||
seen_features.add(identity)
|
||||
related = dict(info)
|
||||
related.update(
|
||||
{
|
||||
"association_source_face_id": candidate_id,
|
||||
"association_hop_count": hop_count,
|
||||
"association_relation": "shared-edge-topology",
|
||||
"association_priority": (
|
||||
0 if surface == "cylinder" else (1 if surface in {"cone", "sphere", "torus"} else 2)
|
||||
),
|
||||
}
|
||||
)
|
||||
results.append(related)
|
||||
|
||||
results.sort(
|
||||
key=lambda item: (
|
||||
int(item.get("association_priority", 9)),
|
||||
int(item.get("association_hop_count", 99)),
|
||||
int(item.get("association_source_face_id", 0)),
|
||||
)
|
||||
)
|
||||
return results[:max_features]
|
||||
|
||||
def _toroidal_feature_info(self, face_id: int, info: dict[str, object]) -> dict[str, object]:
|
||||
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
|
||||
major_radius = float(info.get("major_radius", 0.0) or 0.0)
|
||||
@@ -581,9 +702,11 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
{
|
||||
"kind": "feature",
|
||||
"feature_type": "环面候选",
|
||||
"feature_type": prismatic_info.get("feature_type", "可推拉平面候选"),
|
||||
"feature_source_face_id": face_id,
|
||||
"feature_face_ids": (face_id,),
|
||||
"feature_highlight_face_ids": (face_id,),
|
||||
"feature_highlight_face_ids": tuple(sorted(highlight_face_ids)),
|
||||
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
|
||||
"feature_edit_actions": "修改环面主半径/小半径",
|
||||
"feature_mode": (
|
||||
@@ -622,6 +745,36 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
def _conical_feature_info(self, face_id: int, info: dict[str, object]) -> dict[str, object]:
|
||||
boundary_edge_ids = self._face_boundary_edge_ids(face_id)
|
||||
reference_radius = float(info.get("reference_radius", 0.0) or 0.0)
|
||||
boundary_info: dict[str, object] = {}
|
||||
axis_point = _tuple_or_none(info.get("axis_point"))
|
||||
axis_direction = _tuple_normalized(_tuple_or_none(info.get("axis")))
|
||||
if axis_point is not None and axis_direction is not None:
|
||||
try:
|
||||
circles = self._conical_face_circle_boundaries(face_id, axis_point, axis_direction)
|
||||
except Exception:
|
||||
circles = []
|
||||
if len(circles) == 2:
|
||||
sorted_circles = sorted(circles, key=lambda item: float(item["radius"]))
|
||||
small = sorted_circles[0]
|
||||
large = sorted_circles[1]
|
||||
small_center = tuple(float(value) for value in small["center"])
|
||||
large_center = tuple(float(value) for value in large["center"])
|
||||
height = _vector_length(_tuple_sub(large_center, small_center))
|
||||
small_radius = float(small["radius"])
|
||||
large_radius = float(large["radius"])
|
||||
if height > 1e-9 and large_radius > small_radius > 1e-9:
|
||||
boundary_info.update(
|
||||
{
|
||||
"feature_cone_small_radius": small_radius,
|
||||
"feature_cone_small_diameter": small_radius * 2.0,
|
||||
"feature_cone_large_radius": large_radius,
|
||||
"feature_cone_large_diameter": large_radius * 2.0,
|
||||
"feature_cone_height": height,
|
||||
"feature_cone_boundary_half_angle_degrees": math.degrees(
|
||||
math.atan((large_radius - small_radius) / height)
|
||||
),
|
||||
}
|
||||
)
|
||||
result = dict(info)
|
||||
result.update(
|
||||
{
|
||||
@@ -633,11 +786,12 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"feature_boundary_edge_ids": tuple(boundary_edge_ids),
|
||||
"feature_reference_radius": reference_radius,
|
||||
"feature_reference_diameter": reference_radius * 2.0 if reference_radius > 0 else "",
|
||||
"feature_edit_actions": "修改圆锥参考半径/直径",
|
||||
"feature_edit_actions": "修改圆锥参考半径/直径/半角",
|
||||
"feature_mode": (
|
||||
"这是从 STEP/B-Rep 圆锥面直接识别出的几何候选;修改会围绕圆锥轴做径向缩放,"
|
||||
"不是 CAD 历史里的锥孔或倒角参数。"
|
||||
"这是从 STEP/B-Rep 圆锥面直接识别出的几何候选;简单圆锥会解析重建,"
|
||||
"嵌入式锥孔/沉孔会优先局部重切,不是 CAD 历史里的锥孔或倒角参数。"
|
||||
),
|
||||
**boundary_info,
|
||||
}
|
||||
)
|
||||
return result
|
||||
@@ -646,6 +800,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
coplanar_face_ids = self._connected_coplanar_planar_face_ids(face_id)
|
||||
boundary_edge_ids = self._region_boundary_edge_ids(coplanar_face_ids)
|
||||
shell_info = self._planar_shell_region_info(face_id, coplanar_face_ids, info)
|
||||
prismatic_info = self._planar_rectangular_profile_info(face_id, coplanar_face_ids, info, shell_info)
|
||||
if len(coplanar_face_ids) > 1:
|
||||
scope_note = f"已检测到 {len(coplanar_face_ids)} 个共面且相接/重叠的 face,推拉时会作为同一片平面区域处理。"
|
||||
else:
|
||||
@@ -653,6 +808,14 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
edit_actions = "推拉平面"
|
||||
if shell_info.get("shell_region_status") == "candidate":
|
||||
edit_actions += ";调整薄壁/壳体厚度"
|
||||
if prismatic_info.get("prismatic_profile_status") == "candidate":
|
||||
edit_actions = "调整规则矩形轮廓长度/宽度"
|
||||
if prismatic_info.get("prismatic_extrusion_status") == "candidate":
|
||||
edit_actions += ";调整棱柱高度/凹槽深度"
|
||||
else:
|
||||
edit_actions += ";沿法向推拉"
|
||||
highlight_face_ids = set(coplanar_face_ids)
|
||||
highlight_face_ids.update(_int_values(prismatic_info.get("prismatic_highlight_face_ids")))
|
||||
result = dict(info)
|
||||
result.update(
|
||||
{
|
||||
@@ -671,10 +834,247 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"feature_edit_actions": edit_actions,
|
||||
"feature_mode": "这是从 B-Rep 几何推断出的平面编辑候选,不是 CAD 历史特征。",
|
||||
**shell_info,
|
||||
**prismatic_info,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
def _planar_rectangular_profile_info(
|
||||
self,
|
||||
face_id: int,
|
||||
coplanar_face_ids: Iterable[int],
|
||||
info: dict[str, object],
|
||||
shell_info: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
region_ids = sorted({int(item) for item in coplanar_face_ids})
|
||||
if region_ids != [face_id]:
|
||||
return {
|
||||
"prismatic_profile_status": "not-detected",
|
||||
"prismatic_profile_note": "共面区域包含多个 Face,暂不把整体包围盒当作规则矩形特征尺寸。",
|
||||
}
|
||||
edge_ids = self._face_boundary_edge_ids(face_id)
|
||||
if len(edge_ids) != 4:
|
||||
return {
|
||||
"prismatic_profile_status": "not-detected",
|
||||
"prismatic_profile_note": "规则矩形轮廓需要恰好四条边。",
|
||||
}
|
||||
|
||||
direction_groups: list[dict[str, object]] = []
|
||||
for edge_id in edge_ids:
|
||||
try:
|
||||
curve = BRepAdaptor_Curve(self.edges[edge_id])
|
||||
if curve.GetType() != GeomAbs_Line:
|
||||
return {
|
||||
"prismatic_profile_status": "not-detected",
|
||||
"prismatic_profile_note": "轮廓含非直线边,不按规则矩形特征处理。",
|
||||
}
|
||||
start = _point_tuple(curve.Value(curve.FirstParameter()))
|
||||
end = _point_tuple(curve.Value(curve.LastParameter()))
|
||||
vector = _tuple_sub(end, start)
|
||||
length = math.sqrt(_tuple_dot(vector, vector))
|
||||
direction = _tuple_normalized(vector)
|
||||
except Exception:
|
||||
direction = None
|
||||
length = 0.0
|
||||
if direction is None or length <= 1e-9:
|
||||
return {
|
||||
"prismatic_profile_status": "not-detected",
|
||||
"prismatic_profile_note": "矩形轮廓存在退化边或无法读取的直线边。",
|
||||
}
|
||||
matched_group = None
|
||||
for group in direction_groups:
|
||||
group_direction = _tuple_or_none(group.get("direction"))
|
||||
if group_direction is not None and abs(_tuple_dot(direction, group_direction)) >= 0.999:
|
||||
matched_group = group
|
||||
break
|
||||
if matched_group is None:
|
||||
matched_group = {"direction": direction, "lengths": [], "edge_ids": []}
|
||||
direction_groups.append(matched_group)
|
||||
matched_group["lengths"].append(length)
|
||||
matched_group["edge_ids"].append(edge_id)
|
||||
|
||||
if len(direction_groups) != 2 or any(len(group["lengths"]) != 2 for group in direction_groups):
|
||||
return {
|
||||
"prismatic_profile_status": "not-detected",
|
||||
"prismatic_profile_note": "四条边没有形成两组稳定的平行对边。",
|
||||
}
|
||||
first_direction = _tuple_or_none(direction_groups[0].get("direction"))
|
||||
second_direction = _tuple_or_none(direction_groups[1].get("direction"))
|
||||
if first_direction is None or second_direction is None or abs(_tuple_dot(first_direction, second_direction)) > 0.01:
|
||||
return {
|
||||
"prismatic_profile_status": "not-detected",
|
||||
"prismatic_profile_note": "两组对边不垂直,不按规则矩形特征处理。",
|
||||
}
|
||||
|
||||
for group in direction_groups:
|
||||
lengths = [float(item) for item in group["lengths"]]
|
||||
average = sum(lengths) / len(lengths)
|
||||
if max(abs(item - average) for item in lengths) > max(average * 1e-4, 1e-7):
|
||||
return {
|
||||
"prismatic_profile_status": "not-detected",
|
||||
"prismatic_profile_note": "矩形候选的相对边长度不一致。",
|
||||
}
|
||||
group["average_length"] = average
|
||||
|
||||
direction_groups.sort(key=lambda group: float(group["average_length"]), reverse=True)
|
||||
length = float(direction_groups[0]["average_length"])
|
||||
width = float(direction_groups[1]["average_length"])
|
||||
area = _float_or_none(info.get("area"))
|
||||
area_ratio = area / max(length * width, 1e-12) if area is not None else 0.0
|
||||
if area is None or abs(area_ratio - 1.0) > 0.01:
|
||||
return {
|
||||
"prismatic_profile_status": "not-detected",
|
||||
"prismatic_profile_note": "轮廓面积与长乘宽不一致,可能存在内孔或非矩形裁剪。",
|
||||
"prismatic_profile_area_ratio": area_ratio,
|
||||
}
|
||||
|
||||
adjacent_side_ids = sorted(set(self._adjacent_face_ids_for_edges(edge_ids, face_id)) - {face_id})
|
||||
try:
|
||||
opposite_face_id = int(shell_info["shell_opposite_face_id"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
opposite_face_id = None
|
||||
connected_side_ids: list[int] = []
|
||||
if opposite_face_id is not None:
|
||||
for side_id in adjacent_side_ids:
|
||||
side_neighbors = self._adjacent_face_ids_for_edges(self._face_boundary_edge_ids(side_id), side_id)
|
||||
if opposite_face_id in side_neighbors:
|
||||
connected_side_ids.append(side_id)
|
||||
|
||||
reference_face_ids = [opposite_face_id] if opposite_face_id is not None else []
|
||||
topology_reference = False
|
||||
signed_extrusion = _float_or_none(shell_info.get("shell_signed_thickness"))
|
||||
if len(connected_side_ids) < 2:
|
||||
source_plane = BRepAdaptor_Surface(self.faces[face_id]).Plane()
|
||||
source_normal = source_plane.Axis().Direction()
|
||||
solid_id = self.face_solid_ids[face_id]
|
||||
tolerance = max(_shape_diagonal(self.faces[face_id]) * 1e-6, 1e-6)
|
||||
groups: list[dict[str, object]] = []
|
||||
for side_id in adjacent_side_ids:
|
||||
neighbors = self._adjacent_face_ids_for_edges(self._face_boundary_edge_ids(side_id), side_id)
|
||||
for candidate_id in neighbors:
|
||||
if candidate_id == face_id or candidate_id in region_ids:
|
||||
continue
|
||||
if solid_id >= 0 and self.face_solid_ids[candidate_id] != solid_id:
|
||||
continue
|
||||
try:
|
||||
candidate_surface = BRepAdaptor_Surface(self.faces[candidate_id])
|
||||
if candidate_surface.GetType() != GeomAbs_Plane:
|
||||
continue
|
||||
candidate_plane = candidate_surface.Plane()
|
||||
normal_dot = _direction_dot(source_normal, candidate_plane.Axis().Direction())
|
||||
if abs(normal_dot) < 0.995:
|
||||
continue
|
||||
signed_distance = _axis_parameter(
|
||||
source_plane.Location(),
|
||||
source_normal,
|
||||
candidate_plane.Location(),
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
if abs(signed_distance) <= tolerance:
|
||||
continue
|
||||
matched_group = None
|
||||
for group in groups:
|
||||
if abs(float(group["signed_distance"]) - signed_distance) <= tolerance * 20.0:
|
||||
matched_group = group
|
||||
break
|
||||
if matched_group is None:
|
||||
matched_group = {
|
||||
"signed_distance": signed_distance,
|
||||
"face_ids": set(),
|
||||
"side_ids": set(),
|
||||
"normal_dot": normal_dot,
|
||||
}
|
||||
groups.append(matched_group)
|
||||
matched_group["face_ids"].add(candidate_id)
|
||||
matched_group["side_ids"].add(side_id)
|
||||
|
||||
eligible_groups = [group for group in groups if len(group["side_ids"]) >= 2]
|
||||
if eligible_groups:
|
||||
eligible_groups.sort(key=lambda group: (-len(group["side_ids"]), abs(float(group["signed_distance"]))))
|
||||
best_group = eligible_groups[0]
|
||||
reference_face_ids = sorted(int(item) for item in best_group["face_ids"])
|
||||
connected_side_ids = sorted(int(item) for item in best_group["side_ids"])
|
||||
opposite_face_id = reference_face_ids[0]
|
||||
signed_extrusion = float(best_group["signed_distance"])
|
||||
topology_reference = True
|
||||
support_ratio = len(connected_side_ids) / max(len(adjacent_side_ids), 1)
|
||||
shell_info.update(
|
||||
{
|
||||
"shell_region_status": "candidate",
|
||||
"shell_region_kind": "prismatic-topology-reference",
|
||||
"shell_source_face_ids": tuple(region_ids),
|
||||
"shell_opposite_face_id": opposite_face_id,
|
||||
"shell_thickness_estimate": abs(signed_extrusion),
|
||||
"shell_signed_thickness": signed_extrusion,
|
||||
"shell_overlap_ratio_estimate": support_ratio,
|
||||
"shell_opposite_normal_dot": best_group["normal_dot"],
|
||||
"shell_confidence": "high" if support_ratio >= 0.99 else "medium",
|
||||
"shell_note": "通过矩形轮廓的相邻侧壁找到高度/深度基准。",
|
||||
}
|
||||
)
|
||||
|
||||
extrusion_candidate = opposite_face_id is not None and len(connected_side_ids) >= 2
|
||||
profile_confidence = "high" if len(adjacent_side_ids) == 4 and area_ratio >= 0.999 else "medium"
|
||||
feature_type = "规则矩形棱柱候选" if extrusion_candidate else "规则矩形平面候选"
|
||||
feature_semantics = "generic-prismatic"
|
||||
if extrusion_candidate and reference_face_ids:
|
||||
reference_area = 0.0
|
||||
for reference_id in reference_face_ids:
|
||||
props = GProp_GProps()
|
||||
brepgprop.SurfaceProperties(self.faces[reference_id], props)
|
||||
reference_area += float(props.Mass())
|
||||
oriented_normal = _tuple_normalized(_tuple_or_none(info.get("oriented_normal")))
|
||||
source_normal_tuple = _tuple_normalized(
|
||||
_dir_tuple(BRepAdaptor_Surface(self.faces[face_id]).Plane().Axis().Direction())
|
||||
)
|
||||
if reference_area > area * 1.2 and oriented_normal is not None and source_normal_tuple is not None:
|
||||
outward_offset = float(signed_extrusion or 0.0) * _tuple_dot(source_normal_tuple, oriented_normal)
|
||||
if outward_offset > 0:
|
||||
feature_type = "矩形口袋候选"
|
||||
feature_semantics = "subtractive-pocket"
|
||||
else:
|
||||
feature_type = "矩形凸台候选"
|
||||
feature_semantics = "additive-boss"
|
||||
|
||||
result: dict[str, object] = {
|
||||
"prismatic_profile_status": "candidate",
|
||||
"prismatic_profile_kind": "rectangular-planar-profile",
|
||||
"prismatic_profile_confidence": profile_confidence,
|
||||
"confidence": profile_confidence,
|
||||
"prismatic_length": length,
|
||||
"prismatic_width": width,
|
||||
"prismatic_length_direction": direction_groups[0]["direction"],
|
||||
"prismatic_width_direction": direction_groups[1]["direction"],
|
||||
"prismatic_profile_area_ratio": area_ratio,
|
||||
"prismatic_side_face_ids": tuple(adjacent_side_ids),
|
||||
"prismatic_connected_side_face_ids": tuple(connected_side_ids),
|
||||
"prismatic_reference_face_ids": tuple(reference_face_ids),
|
||||
"prismatic_feature_semantics": feature_semantics,
|
||||
"prismatic_profile_note": "四条直线边形成两组等长平行对边,面积与长乘宽一致。",
|
||||
"feature_type": feature_type,
|
||||
"local_face_width": length,
|
||||
"local_face_height": width,
|
||||
"local_face_width_direction": direction_groups[0]["direction"],
|
||||
"local_face_height_direction": direction_groups[1]["direction"],
|
||||
}
|
||||
if extrusion_candidate:
|
||||
extrusion = _float_or_none(shell_info.get("shell_thickness_estimate"))
|
||||
extrusion_confidence = "high" if len(connected_side_ids) == 4 else "medium"
|
||||
result.update(
|
||||
{
|
||||
"prismatic_extrusion_status": "candidate",
|
||||
"prismatic_extrusion_estimate": extrusion if extrusion is not None else "",
|
||||
"prismatic_reference_face_id": opposite_face_id,
|
||||
"prismatic_extrusion_confidence": extrusion_confidence,
|
||||
"confidence": extrusion_confidence,
|
||||
"prismatic_reference_source": "side-wall-topology" if topology_reference else "overlapping-plane",
|
||||
"prismatic_highlight_face_ids": tuple(sorted({face_id, *reference_face_ids, *connected_side_ids})),
|
||||
"prismatic_extrusion_note": "相对平面通过至少两个侧壁与当前矩形面相连。",
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
def _planar_shell_region_info(
|
||||
self,
|
||||
face_id: int,
|
||||
@@ -768,6 +1168,17 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
|
||||
thickness = float(best["shell_thickness_estimate"])
|
||||
overlap_ratio = float(best["shell_overlap_ratio_estimate"])
|
||||
local_width = _float_or_none(info.get("local_face_width"))
|
||||
local_height = _float_or_none(info.get("local_face_height"))
|
||||
local_spans = [value for value in (local_width, local_height) if value is not None and value > tolerance]
|
||||
if local_spans and thickness > min(local_spans) * 1.5:
|
||||
return {
|
||||
"shell_region_status": "not-detected",
|
||||
"shell_opposite_face_id": best["shell_opposite_face_id"],
|
||||
"shell_thickness_estimate": thickness,
|
||||
"shell_overlap_ratio_estimate": overlap_ratio,
|
||||
"shell_region_note": "相对平面距离明显大于当前面的局部短边,不按薄壁厚度处理。",
|
||||
}
|
||||
thin_ratio = thickness / diagonal
|
||||
if overlap_ratio >= 0.55 and thin_ratio <= 0.08:
|
||||
confidence = "high"
|
||||
@@ -798,15 +1209,39 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
domain_info = dict(info)
|
||||
domain_info["v_range"] = (axis_range["v_min"], axis_range["v_max"])
|
||||
domain_info["height_estimate"] = axis_range["span"]
|
||||
angular_spans: list[float] = []
|
||||
for side_id in side_face_ids:
|
||||
try:
|
||||
side_surface = BRepAdaptor_Surface(self.faces[side_id])
|
||||
angular_spans.append(abs(side_surface.LastUParameter() - side_surface.FirstUParameter()))
|
||||
except Exception:
|
||||
continue
|
||||
combined_angular_span = min(sum(angular_spans), math.tau) if angular_spans else float(info.get("angular_span", 0.0))
|
||||
domain_info["angular_span"] = combined_angular_span
|
||||
end_faces = self._cylindrical_end_face_groups(face_id, adjacent_face_ids, domain_info)
|
||||
end_face_ids = end_faces["end_face_ids"]
|
||||
bottom_face_ids = end_faces["bottom_face_ids"]
|
||||
opening_face_ids = end_faces["opening_face_ids"]
|
||||
guess = str(info.get("feature_guess", "cylindrical face"))
|
||||
has_two_axial_caps = bool(end_faces["start_end_face_ids"] and end_faces["end_end_face_ids"])
|
||||
material_toward = str(info.get("material_toward_axis", "") or "")
|
||||
material_away = str(info.get("material_away_axis", "") or "")
|
||||
if (
|
||||
guess == "round/fillet candidate"
|
||||
and has_two_axial_caps
|
||||
and material_toward == "inside"
|
||||
and "outside" in material_away
|
||||
):
|
||||
info = dict(info)
|
||||
info["feature_guess"] = "boss/outer-round candidate"
|
||||
info["confidence"] = "medium"
|
||||
info["note"] = "partial cylinder has material inside its axis and explicit planar caps at both ends"
|
||||
domain_info["feature_guess"] = info["feature_guess"]
|
||||
slot_info = self._cylindrical_slot_info(face_id, adjacent_face_ids, end_face_ids, domain_info)
|
||||
fillet_info = self._cylindrical_existing_fillet_info(face_id, adjacent_face_ids, end_face_ids, domain_info)
|
||||
|
||||
guess = str(info.get("feature_guess", "cylindrical face"))
|
||||
angular_span = float(info.get("angular_span", 0.0))
|
||||
angular_span = combined_angular_span
|
||||
if guess == "hole/groove candidate":
|
||||
if angular_span < math.tau * 0.92:
|
||||
feature_type = "槽/半孔候选"
|
||||
@@ -856,6 +1291,9 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"feature_adjacent_face_ids": tuple(adjacent_face_ids),
|
||||
"same_domain_v_range": (axis_range["v_min"], axis_range["v_max"]),
|
||||
"same_domain_height_estimate": axis_range["span"],
|
||||
"same_domain_angular_span": combined_angular_span,
|
||||
"angular_span": combined_angular_span,
|
||||
"is_full_cylinder": combined_angular_span >= math.tau * 0.92,
|
||||
"same_domain_range_source": axis_range["range_source"],
|
||||
"same_domain_face_ids": tuple(side_face_ids),
|
||||
"same_domain_face_count": len(side_face_ids),
|
||||
@@ -1141,6 +1579,251 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._face_edge_ids_cache[face_id] = list(edge_ids)
|
||||
return edge_ids
|
||||
|
||||
def face_first_level_topology(self, face_id: int) -> dict[str, object]:
|
||||
"""Return the explicit first-level B-Rep neighborhood for a Face.
|
||||
|
||||
The current project defines first-level Face topology as the selected
|
||||
Face region plus Faces that share a boundary Edge with that region.
|
||||
Vertex-only contacts are reported through boundary vertex counts, but
|
||||
they are not used as propagation edges at this stage.
|
||||
"""
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
raise ValueError(f"Unknown face id {face_id}")
|
||||
cached = self._face_first_level_topology_cache.get(face_id)
|
||||
if cached is not None:
|
||||
return dict(cached)
|
||||
|
||||
source_solid_id = self.face_solid_ids[face_id] if face_id < len(self.face_solid_ids) else -1
|
||||
source_part_id = self.face_part_ids[face_id] if face_id < len(self.face_part_ids) else -1
|
||||
try:
|
||||
same_domain_face_ids = self.connected_same_domain_face_ids(face_id) or [face_id]
|
||||
except Exception:
|
||||
same_domain_face_ids = [face_id]
|
||||
same_domain_face_ids = tuple(sorted({int(item) for item in same_domain_face_ids if 0 <= int(item) < len(self.faces)}))
|
||||
if not same_domain_face_ids:
|
||||
same_domain_face_ids = (face_id,)
|
||||
same_domain_set = set(same_domain_face_ids)
|
||||
|
||||
selected_boundary_edge_ids = tuple(self._face_boundary_edge_ids(face_id))
|
||||
region_boundary_edge_ids = tuple(self._region_boundary_edge_ids(same_domain_face_ids))
|
||||
shared_edges_by_face: dict[int, list[int]] = {}
|
||||
for edge_id in region_boundary_edge_ids:
|
||||
for candidate_id in self._edge_adjacent_face_ids(edge_id):
|
||||
if candidate_id in same_domain_set:
|
||||
continue
|
||||
if source_solid_id >= 0 and self.face_solid_ids[candidate_id] != source_solid_id:
|
||||
continue
|
||||
shared_edges_by_face.setdefault(candidate_id, []).append(edge_id)
|
||||
adjacent_face_ids = tuple(sorted(shared_edges_by_face))
|
||||
first_level_face_ids = tuple(sorted({*same_domain_face_ids, *adjacent_face_ids}))
|
||||
|
||||
diagonal = _shape_diagonal(self.shape)
|
||||
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
|
||||
vertex_points_by_key: dict[tuple[int, int, int], tuple[float, float, float]] = {}
|
||||
for item in same_domain_face_ids:
|
||||
explorer = TopExp_Explorer(self.faces[item], TopAbs_VERTEX)
|
||||
while explorer.More():
|
||||
vertex = topods.Vertex(explorer.Current())
|
||||
point = _point_tuple(BRep_Tool.Pnt(vertex))
|
||||
vertex_points_by_key[self._local_point_key(point, tolerance)] = point
|
||||
explorer.Next()
|
||||
boundary_vertex_points = tuple(vertex_points_by_key[key] for key in sorted(vertex_points_by_key))
|
||||
|
||||
adjacent_surface_types: list[tuple[int, str]] = []
|
||||
for adjacent_id in adjacent_face_ids:
|
||||
adjacent_surface_types.append((adjacent_id, self.face_surface_kind(adjacent_id)))
|
||||
|
||||
shared_edge_refs = tuple(
|
||||
{
|
||||
"face_id": adjacent_id,
|
||||
"edge_ids": tuple(sorted(set(edge_ids))),
|
||||
"edge_count": len(set(edge_ids)),
|
||||
"surface": self.face_surface_kind(adjacent_id),
|
||||
}
|
||||
for adjacent_id, edge_ids in sorted(shared_edges_by_face.items())
|
||||
)
|
||||
topology = {
|
||||
"topology_relation_model": "STEP/B-Rep shared-edge first-level",
|
||||
"topology_relation_depth": 1,
|
||||
"topology_relation_scope": "selected same-domain region + direct shared-edge adjacent Faces",
|
||||
"topology_relation_boundary": "shared-edge",
|
||||
"topology_ignored_relation_depths": ("second-level", "third-level", "deeper"),
|
||||
"topology_ignored_relation_note": (
|
||||
"当前阶段只传播一级关系;相邻 Face 再连接出去的二级、三级拓扑只作为后续目标,不自动递归编辑。"
|
||||
),
|
||||
"source_face_id": face_id,
|
||||
"source_part_id": source_part_id,
|
||||
"source_solid_id": source_solid_id,
|
||||
"same_domain_face_ids": same_domain_face_ids,
|
||||
"same_domain_face_count": len(same_domain_face_ids),
|
||||
"same_domain_region_kind": "same-domain-region" if len(same_domain_face_ids) > 1 else "single-face",
|
||||
"selected_boundary_edge_ids": selected_boundary_edge_ids,
|
||||
"selected_boundary_edge_count": len(selected_boundary_edge_ids),
|
||||
"first_level_boundary_edge_ids": region_boundary_edge_ids,
|
||||
"first_level_boundary_edge_count": len(region_boundary_edge_ids),
|
||||
"first_level_boundary_vertex_points": boundary_vertex_points,
|
||||
"first_level_boundary_vertex_count": len(boundary_vertex_points),
|
||||
"first_level_adjacent_face_ids": adjacent_face_ids,
|
||||
"first_level_adjacent_face_count": len(adjacent_face_ids),
|
||||
"first_level_adjacent_surface_types": tuple(adjacent_surface_types),
|
||||
"first_level_shared_edges_by_face": shared_edge_refs,
|
||||
"first_level_face_ids": first_level_face_ids,
|
||||
"first_level_face_count": len(first_level_face_ids),
|
||||
"first_level_topology_note": (
|
||||
f"已识别当前 Face 区域 {len(same_domain_face_ids)} 个 Face、"
|
||||
f"边界 Edge {len(region_boundary_edge_ids)} 条、"
|
||||
f"边界 Vertex {len(boundary_vertex_points)} 个、"
|
||||
f"共享边一级相邻 Face {len(adjacent_face_ids)} 个;"
|
||||
"当前编辑计划只处理这些一级关系。"
|
||||
),
|
||||
}
|
||||
for item in same_domain_face_ids:
|
||||
self._face_first_level_topology_cache[item] = dict(topology)
|
||||
return dict(topology)
|
||||
|
||||
def cylindrical_feature_first_level_topology(self, face_id: int) -> dict[str, object]:
|
||||
"""Return the first-level B-Rep neighborhood for a cylindrical feature.
|
||||
|
||||
For holes and slots, first-level topology means the selected cylindrical
|
||||
side region and Faces that directly share one of its boundary Edges.
|
||||
Paired slot ends reached through another planar wall are deliberately
|
||||
not promoted to first-level topology at this stage.
|
||||
"""
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
raise ValueError(f"Unknown face id {face_id}")
|
||||
cached = self._cylindrical_first_level_topology_cache.get(face_id)
|
||||
if cached is not None:
|
||||
return dict(cached)
|
||||
|
||||
info = self.face_info(face_id)
|
||||
if info.get("surface") != "cylinder":
|
||||
raise ValueError(f"Face {face_id} is not a cylindrical feature face")
|
||||
|
||||
source_solid_id = self.face_solid_ids[face_id] if face_id < len(self.face_solid_ids) else -1
|
||||
source_part_id = self.face_part_ids[face_id] if face_id < len(self.face_part_ids) else -1
|
||||
feature = self.feature_info(face_id)
|
||||
|
||||
def valid_face_ids(values: object) -> tuple[int, ...]:
|
||||
result: list[int] = []
|
||||
for item in _int_values(values):
|
||||
if item < 0 or item >= len(self.faces):
|
||||
continue
|
||||
if source_part_id >= 0 and self.face_part_ids[item] != source_part_id:
|
||||
continue
|
||||
if source_solid_id >= 0 and self.face_solid_ids[item] != source_solid_id:
|
||||
continue
|
||||
result.append(item)
|
||||
return tuple(sorted(set(result)))
|
||||
|
||||
side_face_ids = valid_face_ids(feature.get("feature_side_face_ids"))
|
||||
if not side_face_ids:
|
||||
side_face_ids = valid_face_ids(feature.get("feature_face_ids"))
|
||||
if not side_face_ids:
|
||||
try:
|
||||
side_face_ids = valid_face_ids(self.connected_same_domain_face_ids(face_id))
|
||||
except Exception:
|
||||
side_face_ids = ()
|
||||
if not side_face_ids:
|
||||
side_face_ids = (face_id,)
|
||||
side_face_set = set(side_face_ids)
|
||||
|
||||
selected_boundary_edge_ids = tuple(self._face_boundary_edge_ids(face_id))
|
||||
region_boundary_edge_ids = tuple(self._region_boundary_edge_ids(side_face_ids))
|
||||
shared_edges_by_face: dict[int, list[int]] = {}
|
||||
for edge_id in region_boundary_edge_ids:
|
||||
for candidate_id in self._edge_adjacent_face_ids(edge_id):
|
||||
if candidate_id in side_face_set:
|
||||
continue
|
||||
if source_part_id >= 0 and self.face_part_ids[candidate_id] != source_part_id:
|
||||
continue
|
||||
if source_solid_id >= 0 and self.face_solid_ids[candidate_id] != source_solid_id:
|
||||
continue
|
||||
shared_edges_by_face.setdefault(candidate_id, []).append(edge_id)
|
||||
adjacent_face_ids = tuple(sorted(shared_edges_by_face))
|
||||
|
||||
end_face_ids = valid_face_ids(feature.get("feature_end_face_ids"))
|
||||
bottom_face_ids = valid_face_ids(feature.get("feature_bottom_face_ids"))
|
||||
opening_face_ids = valid_face_ids(feature.get("feature_opening_face_ids"))
|
||||
slot_boundary_face_ids = valid_face_ids(feature.get("feature_slot_boundary_face_ids"))
|
||||
first_level_face_ids = tuple(sorted({*side_face_ids, *adjacent_face_ids}))
|
||||
|
||||
diagonal = _shape_diagonal(self.shape)
|
||||
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
|
||||
vertex_points_by_key: dict[tuple[int, int, int], tuple[float, float, float]] = {}
|
||||
for edge_id in region_boundary_edge_ids:
|
||||
if edge_id < 0 or edge_id >= len(self.edges):
|
||||
continue
|
||||
explorer = TopExp_Explorer(self.edges[edge_id], TopAbs_VERTEX)
|
||||
while explorer.More():
|
||||
vertex = topods.Vertex(explorer.Current())
|
||||
point = _point_tuple(BRep_Tool.Pnt(vertex))
|
||||
vertex_points_by_key[self._local_point_key(point, tolerance)] = point
|
||||
explorer.Next()
|
||||
boundary_vertex_points = tuple(vertex_points_by_key[key] for key in sorted(vertex_points_by_key))
|
||||
|
||||
adjacent_surface_types = tuple((item, self.face_surface_kind(item)) for item in adjacent_face_ids)
|
||||
shared_edge_refs = tuple(
|
||||
{
|
||||
"face_id": adjacent_id,
|
||||
"edge_ids": tuple(sorted(set(edge_ids))),
|
||||
"edge_count": len(set(edge_ids)),
|
||||
"surface": self.face_surface_kind(adjacent_id),
|
||||
}
|
||||
for adjacent_id, edge_ids in sorted(shared_edges_by_face.items())
|
||||
)
|
||||
|
||||
angular_span = feature.get("slot_angular_span", feature.get("angular_span", info.get("angular_span")))
|
||||
topology = {
|
||||
"topology_relation_model": "STEP/B-Rep cylindrical-feature shared-edge first-level",
|
||||
"topology_relation_depth": 1,
|
||||
"topology_relation_scope": "selected cylindrical same-domain side region + direct shared-edge adjacent Faces",
|
||||
"topology_relation_boundary": "shared-edge",
|
||||
"topology_ignored_relation_depths": ("second-level", "third-level", "deeper"),
|
||||
"topology_ignored_relation_note": (
|
||||
"Only direct shared-edge neighbors of the cylindrical side region are treated as first-level topology. "
|
||||
"Faces reached through those neighbors are recorded later as second-level or deeper relationships."
|
||||
),
|
||||
"source_face_id": face_id,
|
||||
"source_part_id": source_part_id,
|
||||
"source_solid_id": source_solid_id,
|
||||
"feature_type": feature.get("feature_type"),
|
||||
"feature_guess": feature.get("feature_guess", info.get("feature_guess")),
|
||||
"slot_kind": feature.get("slot_kind", ""),
|
||||
"cylinder_end_type": feature.get("cylinder_end_type", info.get("cylinder_end_type")),
|
||||
"is_full_cylinder": bool(feature.get("is_full_cylinder", info.get("is_full_cylinder", False))),
|
||||
"angular_span": angular_span,
|
||||
"cylindrical_feature_side_face_ids": side_face_ids,
|
||||
"cylindrical_feature_side_face_count": len(side_face_ids),
|
||||
"cylindrical_feature_selected_boundary_edge_ids": selected_boundary_edge_ids,
|
||||
"cylindrical_feature_selected_boundary_edge_count": len(selected_boundary_edge_ids),
|
||||
"cylindrical_feature_boundary_edge_ids": region_boundary_edge_ids,
|
||||
"cylindrical_feature_boundary_edge_count": len(region_boundary_edge_ids),
|
||||
"cylindrical_feature_boundary_vertex_points": boundary_vertex_points,
|
||||
"cylindrical_feature_boundary_vertex_count": len(boundary_vertex_points),
|
||||
"cylindrical_feature_adjacent_face_ids": adjacent_face_ids,
|
||||
"cylindrical_feature_adjacent_face_count": len(adjacent_face_ids),
|
||||
"cylindrical_feature_adjacent_surface_types": adjacent_surface_types,
|
||||
"cylindrical_feature_shared_edges_by_face": shared_edge_refs,
|
||||
"cylindrical_feature_first_level_face_ids": first_level_face_ids,
|
||||
"cylindrical_feature_first_level_face_count": len(first_level_face_ids),
|
||||
"cylindrical_feature_end_face_ids": end_face_ids,
|
||||
"cylindrical_feature_end_face_count": len(end_face_ids),
|
||||
"cylindrical_feature_bottom_face_ids": bottom_face_ids,
|
||||
"cylindrical_feature_bottom_face_count": len(bottom_face_ids),
|
||||
"cylindrical_feature_opening_face_ids": opening_face_ids,
|
||||
"cylindrical_feature_opening_face_count": len(opening_face_ids),
|
||||
"cylindrical_feature_slot_boundary_face_ids": slot_boundary_face_ids,
|
||||
"cylindrical_feature_slot_boundary_face_count": len(slot_boundary_face_ids),
|
||||
"first_level_topology_note": (
|
||||
f"Cylindrical side Faces={len(side_face_ids)}, boundary Edges={len(region_boundary_edge_ids)}, "
|
||||
f"boundary Vertices={len(boundary_vertex_points)}, direct adjacent Faces={len(adjacent_face_ids)}. "
|
||||
"Second-level and deeper propagation is not automatic in this stage."
|
||||
),
|
||||
}
|
||||
for item in side_face_ids:
|
||||
self._cylindrical_first_level_topology_cache[item] = dict(topology)
|
||||
return dict(topology)
|
||||
|
||||
def _face_boundary_wire_info(self, face: TopoDS_Shape) -> dict[str, object]:
|
||||
try:
|
||||
boundary_wires = len(_explore(face, TopAbs_WIRE))
|
||||
|
||||
Reference in New Issue
Block a user