feat: 完善 Face 一级关系编辑和稳定性校验
This commit is contained in:
+441
-32
@@ -113,6 +113,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
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._face_first_level_fact_cache: dict[tuple[int, str], 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
|
||||
@@ -196,6 +197,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._same_domain_face_ids_cache.clear()
|
||||
self._face_first_level_topology_cache.clear()
|
||||
self._cylindrical_first_level_topology_cache.clear()
|
||||
self._face_first_level_fact_cache.clear()
|
||||
self._local_face_deform_readiness_cache.clear()
|
||||
self._edge_duplicate_key_ids_cache = None
|
||||
self._same_domain_internal_edge_ids_cache = None
|
||||
@@ -290,6 +292,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._same_domain_face_ids_cache.clear()
|
||||
self._face_first_level_topology_cache.clear()
|
||||
self._cylindrical_first_level_topology_cache.clear()
|
||||
self._face_first_level_fact_cache.clear()
|
||||
self._local_face_deform_readiness_cache.clear()
|
||||
|
||||
def _restore_face_logical_ids_if_count_matches(self, logical_ids: Iterable[int]) -> bool:
|
||||
@@ -303,6 +306,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._same_domain_face_ids_cache.clear()
|
||||
self._face_first_level_topology_cache.clear()
|
||||
self._cylindrical_first_level_topology_cache.clear()
|
||||
self._face_first_level_fact_cache.clear()
|
||||
self._local_face_deform_readiness_cache.clear()
|
||||
return True
|
||||
|
||||
@@ -349,12 +353,23 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
info["oriented_normal"] = _oriented_dir_tuple(direction, face)
|
||||
info["push_pull_confidence"] = "unchecked"
|
||||
info["push_pull_note"] = "快速选择阶段不判断材料内外方向;执行推拉时会重新计算。"
|
||||
cap_direction = self._cylindrical_cap_push_pull_direction(face_id, surf)
|
||||
if cap_direction is not None:
|
||||
info.update(cap_direction)
|
||||
info["push_pull_status"] = "candidate"
|
||||
info["feature_type"] = "可推拉平面候选"
|
||||
info["feature_source_face_id"] = face_id
|
||||
info["feature_highlight_face_ids"] = (face_id,)
|
||||
info["feature_edit_actions"] = "推拉平面"
|
||||
info.update(self._local_face_deform_readiness(face_id))
|
||||
if bool(info.get("has_inner_boundaries")):
|
||||
info["local_face_deform_ready"] = False
|
||||
info["local_face_deform_face_count"] = 1
|
||||
info["local_face_deform_blocker"] = "当前 Face 有内孔/内边界;请优先使用推拉当前面、孔或槽的专门修改入口。"
|
||||
else:
|
||||
info["local_face_deform_ready"] = True
|
||||
info["local_face_deform_face_count"] = 1
|
||||
info["local_face_deform_blocker"] = ""
|
||||
info["local_face_deform_status"] = "deferred"
|
||||
info.update(
|
||||
self._local_face_plane_size_info(
|
||||
face_id,
|
||||
@@ -416,6 +431,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
info["feature_torus_major_radius"] = torus.MajorRadius()
|
||||
info["feature_torus_minor_radius"] = torus.MinorRadius()
|
||||
|
||||
info.update(self._recognition_summary_fields(info))
|
||||
self._quick_face_info_cache[face_id] = dict(info)
|
||||
return dict(info)
|
||||
|
||||
@@ -512,6 +528,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
info["axis"] = _dir_tuple(torus.Axis().Direction())
|
||||
info["major_radius"] = torus.MajorRadius()
|
||||
info["minor_radius"] = torus.MinorRadius()
|
||||
info.update(self._recognition_summary_fields(info))
|
||||
self._face_info_cache[face_id] = dict(info)
|
||||
return dict(info)
|
||||
|
||||
@@ -545,6 +562,267 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _recognition_summary_fields(self, info: dict[str, object]) -> dict[str, object]:
|
||||
surface = str(info.get("surface") or "")
|
||||
candidate = (
|
||||
str(info.get("feature_type") or "").strip()
|
||||
or str(info.get("feature_guess") or "").strip()
|
||||
or (f"{surface} Face" if surface else "Face")
|
||||
)
|
||||
confidence = (
|
||||
str(info.get("confidence") or "").strip()
|
||||
or str(info.get("push_pull_confidence") or "").strip()
|
||||
or str(info.get("shell_confidence") or "").strip()
|
||||
or "unchecked"
|
||||
)
|
||||
if confidence == "unchecked":
|
||||
feature_guess = str(info.get("feature_guess") or "")
|
||||
has_axis = info.get("axis") not in {None, ""} or info.get("axis_point") not in {None, ""}
|
||||
if surface == "plane" and info.get("boundary_edges") not in {None, ""}:
|
||||
confidence = "high"
|
||||
elif surface == "cylinder" and _float_or_none(info.get("radius")) is not None and has_axis:
|
||||
confidence = "high" if feature_guess else "medium"
|
||||
elif surface == "cone" and _float_or_none(info.get("reference_radius")) is not None and _float_or_none(info.get("semi_angle")) is not None:
|
||||
confidence = "medium"
|
||||
elif surface == "sphere" and _float_or_none(info.get("radius")) is not None:
|
||||
confidence = "high"
|
||||
elif surface == "torus" and _float_or_none(info.get("major_radius")) is not None and _float_or_none(info.get("minor_radius")) is not None:
|
||||
confidence = "high"
|
||||
|
||||
risk_rank = {"low": 0, "medium": 1, "high": 2, "blocked": 3}
|
||||
capability_specs = (
|
||||
("resize_status", "resize_risk", "resize_blockers", "孔/槽/圆柱直径"),
|
||||
("push_pull_status", "push_pull_risk", "push_pull_blockers", "平面推拉"),
|
||||
("shell_status", "shell_risk", "shell_blockers", "薄壁厚度"),
|
||||
("cylinder_resize_status", "cylinder_resize_risk", "cylinder_resize_blockers", "圆柱直径/半径"),
|
||||
("boss_resize_status", "boss_resize_risk", "boss_resize_blockers", "圆柱凸台直径/高度/轴心"),
|
||||
("depth_status", "depth_risk", "depth_blockers", "盲孔/盲槽深度"),
|
||||
("suppress_status", "suppress_risk", "suppress_blockers", "封堵孔/槽"),
|
||||
("fillet_status", "fillet_risk", "fillet_blockers", "已有圆角半径"),
|
||||
("chamfer_status", "chamfer_risk", "chamfer_blockers", "倒角"),
|
||||
)
|
||||
available_capabilities = {
|
||||
status_key
|
||||
for status_key, _risk_key, _blocker_key, _label in capability_specs
|
||||
if str(info.get(status_key) or "").strip() in {"ready", "caution", "candidate"}
|
||||
}
|
||||
feature_type_text = str(info.get("feature_type") or "")
|
||||
feature_actions_text = str(info.get("feature_edit_actions") or "")
|
||||
has_planar_push_pull_candidate = (
|
||||
surface == "plane"
|
||||
and (
|
||||
"推拉" in feature_type_text
|
||||
or "推拉" in feature_actions_text
|
||||
or str(info.get("push_pull_status") or "").strip() == "candidate"
|
||||
)
|
||||
)
|
||||
has_local_face_deform = bool(info.get("local_face_deform_ready"))
|
||||
has_slot_candidate = str(info.get("slot_status") or "").strip() == "candidate"
|
||||
has_shell_candidate = str(info.get("shell_region_status") or "").strip() == "candidate"
|
||||
has_analytic_surface_candidate = surface in {"cone", "sphere", "torus"} and bool(feature_type_text)
|
||||
has_available_capability = bool(
|
||||
available_capabilities
|
||||
or has_planar_push_pull_candidate
|
||||
or has_local_face_deform
|
||||
or has_slot_candidate
|
||||
or has_shell_candidate
|
||||
or has_analytic_surface_candidate
|
||||
)
|
||||
risk = "low"
|
||||
for key in (
|
||||
"risk",
|
||||
"first_level_topology_risk",
|
||||
):
|
||||
value = str(info.get(key) or "").strip()
|
||||
if risk_rank.get(value, -1) > risk_rank.get(risk, -1):
|
||||
risk = value
|
||||
for status_key, risk_key, _blocker_key, _label in capability_specs:
|
||||
value = str(info.get(risk_key) or "").strip()
|
||||
if value == "blocked" and has_available_capability:
|
||||
continue
|
||||
if risk_rank.get(value, -1) > risk_rank.get(risk, -1):
|
||||
risk = value
|
||||
if confidence in {"low", "unchecked", "none"} and risk == "low":
|
||||
risk = "medium"
|
||||
|
||||
evidence: list[str] = []
|
||||
evidence_keys: list[str] = []
|
||||
|
||||
def add(key: str, text: str) -> None:
|
||||
if text and key not in evidence_keys:
|
||||
evidence_keys.append(key)
|
||||
evidence.append(text)
|
||||
|
||||
if surface:
|
||||
add("surface", f"曲面={surface}")
|
||||
if surface == "cylinder" and info.get("radius") not in {None, ""}:
|
||||
add("cylinder_geometry", f"圆柱半径={info.get('radius')}")
|
||||
if surface == "cone":
|
||||
if info.get("reference_radius") not in {None, ""}:
|
||||
add("cone_geometry", f"圆锥参考半径={info.get('reference_radius')}")
|
||||
if info.get("semi_angle") not in {None, ""}:
|
||||
add("cone_angle", f"圆锥半角={info.get('semi_angle')}")
|
||||
if surface == "sphere" and info.get("radius") not in {None, ""}:
|
||||
add("sphere_geometry", f"球半径={info.get('radius')}")
|
||||
if surface == "torus":
|
||||
if info.get("major_radius") not in {None, ""}:
|
||||
add("torus_major_radius", f"环面主半径={info.get('major_radius')}")
|
||||
if info.get("minor_radius") not in {None, ""}:
|
||||
add("torus_minor_radius", f"环面小半径={info.get('minor_radius')}")
|
||||
if info.get("boundary_edges") not in {None, ""}:
|
||||
add("boundary_edges", f"边界Edge={info.get('boundary_edges')}")
|
||||
if info.get("same_domain_face_count") not in {None, ""}:
|
||||
add("same_domain", f"同域Face={info.get('same_domain_face_count')}")
|
||||
if info.get("first_level_adjacent_face_count") not in {None, ""}:
|
||||
add("first_level_topology", f"一级相邻Face={info.get('first_level_adjacent_face_count')}")
|
||||
elif info.get("feature_adjacent_face_ids") not in {None, ""}:
|
||||
try:
|
||||
adjacent_count = len(tuple(info.get("feature_adjacent_face_ids") or ()))
|
||||
except TypeError:
|
||||
adjacent_count = 0
|
||||
add("first_level_topology", f"一级相邻Face={adjacent_count}")
|
||||
if info.get("first_level_fact_summary") not in {None, ""}:
|
||||
add("first_level_fact_graph", f"一级事实={info.get('first_level_fact_summary')}")
|
||||
if info.get("material_vote_summary") not in {None, ""}:
|
||||
add("material_votes", f"材料采样={info.get('material_vote_summary')}")
|
||||
if info.get("feature_end_face_ids") not in {None, ""}:
|
||||
try:
|
||||
end_count = len(tuple(info.get("feature_end_face_ids") or ()))
|
||||
except TypeError:
|
||||
end_count = 0
|
||||
add("end_faces", f"端面Face={end_count}")
|
||||
if info.get("feature_bottom_face_ids") not in {None, ""}:
|
||||
try:
|
||||
bottom_count = len(tuple(info.get("feature_bottom_face_ids") or ()))
|
||||
except TypeError:
|
||||
bottom_count = 0
|
||||
add("bottom_faces", f"疑似底面Face={bottom_count}")
|
||||
if info.get("slot_status") == "candidate":
|
||||
add("slot_geometry", "部分圆柱槽/半孔几何")
|
||||
if info.get("shell_region_status") == "candidate":
|
||||
add("shell_opposite_face", "找到相对平面/薄壁候选")
|
||||
|
||||
ready_actions: list[str] = []
|
||||
limited_actions: list[str] = []
|
||||
blockers: list[str] = []
|
||||
limitations: list[str] = []
|
||||
|
||||
def add_unique(items: list[str], text: str) -> None:
|
||||
if text and text not in items:
|
||||
items.append(text)
|
||||
|
||||
def add_action(items: list[str], text: str) -> None:
|
||||
if text and text not in items:
|
||||
items.append(text)
|
||||
|
||||
for status_key, _risk_key, blocker_key, label in capability_specs:
|
||||
status = str(info.get(status_key) or "").strip()
|
||||
blocker_text = str(info.get(blocker_key) or "").strip()
|
||||
if status in {"ready", "caution", "candidate"}:
|
||||
add_action(ready_actions, label)
|
||||
elif status == "blocked" and blocker_text:
|
||||
add_action(limited_actions, label)
|
||||
|
||||
if has_planar_push_pull_candidate:
|
||||
add_action(ready_actions, "平面推拉")
|
||||
if has_local_face_deform:
|
||||
add_action(ready_actions, "当前面面积/面宽/面高/中心/偏移")
|
||||
elif str(info.get("local_face_deform_blocker") or "").strip():
|
||||
add_action(limited_actions, "当前面局部尺寸/中心/偏移")
|
||||
if has_shell_candidate:
|
||||
add_action(ready_actions, "薄壁厚度")
|
||||
if has_slot_candidate:
|
||||
add_action(ready_actions, "槽/半孔宽度/深度/弧长")
|
||||
if surface == "cone" and feature_type_text:
|
||||
add_action(ready_actions, "圆锥参考半径/直径/半角")
|
||||
elif surface == "sphere" and feature_type_text:
|
||||
add_action(ready_actions, "球面半径/直径")
|
||||
elif surface == "torus" and feature_type_text:
|
||||
add_action(ready_actions, "环面主/小半径或直径")
|
||||
|
||||
for key in ("local_face_deform_blocker", "first_level_topology_blockers"):
|
||||
text = str(info.get(key) or "").strip()
|
||||
if not text:
|
||||
continue
|
||||
if has_available_capability:
|
||||
add_unique(limitations, text)
|
||||
else:
|
||||
add_unique(blockers, text)
|
||||
for status_key, _risk_key, blocker_key, _label in capability_specs:
|
||||
text = str(info.get(blocker_key) or "").strip()
|
||||
if not text:
|
||||
continue
|
||||
status = str(info.get(status_key) or "").strip()
|
||||
if status == "blocked" and has_available_capability:
|
||||
add_unique(limitations, text)
|
||||
else:
|
||||
add_unique(blockers, text)
|
||||
|
||||
note = (
|
||||
str(info.get("feature_mode") or "").strip()
|
||||
or str(info.get("note") or "").strip()
|
||||
or str(info.get("push_pull_note") or "").strip()
|
||||
)
|
||||
if note:
|
||||
add("note", note)
|
||||
|
||||
confidence_points = {"high": 72, "medium": 56, "low": 34, "unchecked": 22, "none": 0}
|
||||
risk_penalty = {"low": 0, "medium": 14, "high": 30, "blocked": 72}
|
||||
score = confidence_points.get(confidence, 22)
|
||||
score += min(len(evidence_keys) * 5, 24)
|
||||
score -= risk_penalty.get(risk, 14)
|
||||
if blockers:
|
||||
score -= 35
|
||||
elif limitations:
|
||||
score -= min(len(limitations) * 4, 12)
|
||||
score = max(0, min(100, int(round(score))))
|
||||
|
||||
if risk == "blocked" or blockers:
|
||||
decision = "已阻止"
|
||||
elif score >= 76 and risk == "low":
|
||||
decision = "高可信候选"
|
||||
elif score >= 56:
|
||||
decision = "可尝试候选"
|
||||
elif score >= 36:
|
||||
decision = "需人工确认"
|
||||
else:
|
||||
decision = "不建议自动修改"
|
||||
|
||||
summary_parts = [candidate, f"置信度={confidence}", f"风险={risk}", f"评分={score}", f"结论={decision}"]
|
||||
if evidence:
|
||||
summary_evidence = list(evidence[:5])
|
||||
if "first_level_fact_graph" in evidence_keys:
|
||||
fact_text = evidence[evidence_keys.index("first_level_fact_graph")]
|
||||
if fact_text not in summary_evidence:
|
||||
if len(summary_evidence) >= 5:
|
||||
summary_evidence[-1] = fact_text
|
||||
else:
|
||||
summary_evidence.append(fact_text)
|
||||
summary_parts.append("证据:" + ";".join(summary_evidence))
|
||||
if ready_actions:
|
||||
summary_parts.append("可改:" + ";".join(ready_actions[:4]))
|
||||
if blockers:
|
||||
summary_parts.append("限制:" + ";".join(blockers[:2]))
|
||||
elif limitations:
|
||||
summary_parts.append("受限能力:" + ";".join(limitations[:2]))
|
||||
if limited_actions:
|
||||
summary_parts.append("受限修改:" + ";".join(limited_actions[:4]))
|
||||
|
||||
return {
|
||||
"recognition_candidate": candidate,
|
||||
"recognition_confidence": confidence,
|
||||
"recognition_risk": risk,
|
||||
"recognition_score": score,
|
||||
"recognition_decision": decision,
|
||||
"recognition_evidence": ";".join(evidence),
|
||||
"recognition_evidence_keys": tuple(evidence_keys),
|
||||
"recognition_ready_actions": ";".join(ready_actions),
|
||||
"recognition_limited_actions": ";".join(limited_actions),
|
||||
"recognition_blockers": ";".join(blockers),
|
||||
"recognition_limitations": ";".join(limitations),
|
||||
"recognition_summary": ";".join(summary_parts),
|
||||
}
|
||||
|
||||
def cached_feature_info(self, face_id: int) -> dict[str, object] | None:
|
||||
cached = self._feature_info_cache.get(face_id)
|
||||
return dict(cached) if cached is not None else None
|
||||
@@ -584,6 +862,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
),
|
||||
}
|
||||
)
|
||||
result.update(self._recognition_summary_fields(result))
|
||||
self._feature_info_cache[face_id] = dict(result)
|
||||
return dict(result)
|
||||
|
||||
@@ -702,11 +981,9 @@ 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": (
|
||||
@@ -1824,6 +2101,133 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._cylindrical_first_level_topology_cache[item] = dict(topology)
|
||||
return dict(topology)
|
||||
|
||||
def face_first_level_facts(self, face_id: int, scope: str = "auto") -> dict[str, object]:
|
||||
"""Return a unified first-level fact graph for recognition and UI.
|
||||
|
||||
This is intentionally a fact layer, not a feature-history guess. It
|
||||
normalizes the selected region, boundary Edges/Vertices, direct
|
||||
shared-edge neighbors, and ignored deeper relation depths so Face,
|
||||
cylinder, hole, slot, and later feature recognizers can share the same
|
||||
evidence contract.
|
||||
"""
|
||||
if face_id < 0 or face_id >= len(self.faces):
|
||||
raise ValueError(f"Unknown face id {face_id}")
|
||||
requested_scope = str(scope or "auto")
|
||||
surface = self.face_surface_kind(face_id)
|
||||
resolved_scope = requested_scope
|
||||
if requested_scope == "auto":
|
||||
resolved_scope = "cylindrical-feature" if surface == "cylinder" else "face"
|
||||
if resolved_scope not in {"face", "cylindrical-feature"}:
|
||||
raise ValueError(f"Unknown first-level fact scope: {scope}")
|
||||
cache_key = (int(face_id), resolved_scope)
|
||||
cached = self._face_first_level_fact_cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return dict(cached)
|
||||
|
||||
if resolved_scope == "cylindrical-feature":
|
||||
topology = self.cylindrical_feature_first_level_topology(face_id)
|
||||
subject_face_ids = tuple(_int_values(topology.get("cylindrical_feature_side_face_ids"))) or (face_id,)
|
||||
boundary_edge_ids = tuple(_int_values(topology.get("cylindrical_feature_boundary_edge_ids")))
|
||||
boundary_vertex_points = tuple(topology.get("cylindrical_feature_boundary_vertex_points") or ())
|
||||
adjacent_face_ids = tuple(_int_values(topology.get("cylindrical_feature_adjacent_face_ids")))
|
||||
included_face_ids = tuple(_int_values(topology.get("cylindrical_feature_first_level_face_ids"))) or tuple(
|
||||
sorted({*subject_face_ids, *adjacent_face_ids})
|
||||
)
|
||||
role_groups = (
|
||||
{
|
||||
"role": "cylindrical-side",
|
||||
"face_ids": subject_face_ids,
|
||||
"count": len(subject_face_ids),
|
||||
},
|
||||
{
|
||||
"role": "direct-adjacent",
|
||||
"face_ids": adjacent_face_ids,
|
||||
"count": len(adjacent_face_ids),
|
||||
},
|
||||
{
|
||||
"role": "end/opening",
|
||||
"face_ids": tuple(_int_values(topology.get("cylindrical_feature_end_face_ids"))),
|
||||
"count": int(topology.get("cylindrical_feature_end_face_count", 0) or 0),
|
||||
},
|
||||
{
|
||||
"role": "blind-bottom",
|
||||
"face_ids": tuple(_int_values(topology.get("cylindrical_feature_bottom_face_ids"))),
|
||||
"count": int(topology.get("cylindrical_feature_bottom_face_count", 0) or 0),
|
||||
},
|
||||
{
|
||||
"role": "slot-boundary",
|
||||
"face_ids": tuple(_int_values(topology.get("cylindrical_feature_slot_boundary_face_ids"))),
|
||||
"count": int(topology.get("cylindrical_feature_slot_boundary_face_count", 0) or 0),
|
||||
},
|
||||
)
|
||||
subject_role = "cylindrical side region"
|
||||
source_model = "cylindrical-feature"
|
||||
else:
|
||||
topology = self.face_first_level_topology(face_id)
|
||||
subject_face_ids = tuple(_int_values(topology.get("same_domain_face_ids"))) or (face_id,)
|
||||
boundary_edge_ids = tuple(_int_values(topology.get("first_level_boundary_edge_ids")))
|
||||
boundary_vertex_points = tuple(topology.get("first_level_boundary_vertex_points") or ())
|
||||
adjacent_face_ids = tuple(_int_values(topology.get("first_level_adjacent_face_ids")))
|
||||
included_face_ids = tuple(_int_values(topology.get("first_level_face_ids"))) or tuple(
|
||||
sorted({*subject_face_ids, *adjacent_face_ids})
|
||||
)
|
||||
role_groups = (
|
||||
{
|
||||
"role": "selected-same-domain-region",
|
||||
"face_ids": subject_face_ids,
|
||||
"count": len(subject_face_ids),
|
||||
},
|
||||
{
|
||||
"role": "direct-adjacent",
|
||||
"face_ids": adjacent_face_ids,
|
||||
"count": len(adjacent_face_ids),
|
||||
},
|
||||
)
|
||||
subject_role = "selected same-domain Face region"
|
||||
source_model = "face"
|
||||
|
||||
adjacent_surface_types = tuple(topology.get("first_level_adjacent_surface_types") or ()) or tuple(
|
||||
topology.get("cylindrical_feature_adjacent_surface_types") or ()
|
||||
)
|
||||
shared_edges = tuple(topology.get("first_level_shared_edges_by_face") or ()) or tuple(
|
||||
topology.get("cylindrical_feature_shared_edges_by_face") or ()
|
||||
)
|
||||
ignored_depths = tuple(topology.get("topology_ignored_relation_depths") or ("second-level", "third-level"))
|
||||
summary = (
|
||||
f"{subject_role}: Face {len(subject_face_ids)}, boundary Edge {len(boundary_edge_ids)}, "
|
||||
f"boundary Vertex {len(boundary_vertex_points)}, direct adjacent Face {len(adjacent_face_ids)}; "
|
||||
"deeper relations are recorded as future propagation targets, not edited automatically."
|
||||
)
|
||||
facts = {
|
||||
"first_level_fact_model": "STEP/B-Rep first-level fact graph",
|
||||
"first_level_fact_source_model": source_model,
|
||||
"first_level_fact_status": "ready",
|
||||
"first_level_fact_relation_depth": 1,
|
||||
"first_level_fact_relation_boundary": "shared-edge",
|
||||
"first_level_fact_scope": resolved_scope,
|
||||
"first_level_fact_subject_role": subject_role,
|
||||
"first_level_fact_subject_face_ids": tuple(sorted(set(subject_face_ids))),
|
||||
"first_level_fact_subject_face_count": len(set(subject_face_ids)),
|
||||
"first_level_fact_boundary_edge_ids": tuple(sorted(set(boundary_edge_ids))),
|
||||
"first_level_fact_boundary_edge_count": len(set(boundary_edge_ids)),
|
||||
"first_level_fact_boundary_vertex_points": boundary_vertex_points,
|
||||
"first_level_fact_boundary_vertex_count": len(boundary_vertex_points),
|
||||
"first_level_fact_adjacent_face_ids": tuple(sorted(set(adjacent_face_ids))),
|
||||
"first_level_fact_adjacent_face_count": len(set(adjacent_face_ids)),
|
||||
"first_level_fact_adjacent_surface_types": adjacent_surface_types,
|
||||
"first_level_fact_shared_edges_by_face": shared_edges,
|
||||
"first_level_fact_included_face_ids": tuple(sorted(set(included_face_ids))),
|
||||
"first_level_fact_included_face_count": len(set(included_face_ids)),
|
||||
"first_level_fact_role_groups": role_groups,
|
||||
"first_level_fact_ignored_relation_depths": ignored_depths,
|
||||
"first_level_fact_ignored_relation_note": topology.get("topology_ignored_relation_note", ""),
|
||||
"first_level_fact_summary": summary,
|
||||
}
|
||||
for item in facts["first_level_fact_subject_face_ids"]:
|
||||
self._face_first_level_fact_cache[(int(item), resolved_scope)] = dict(facts)
|
||||
self._face_first_level_fact_cache[cache_key] = dict(facts)
|
||||
return dict(facts)
|
||||
|
||||
def _face_boundary_wire_info(self, face: TopoDS_Shape) -> dict[str, object]:
|
||||
try:
|
||||
boundary_wires = len(_explore(face, TopAbs_WIRE))
|
||||
@@ -2109,6 +2513,23 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
|
||||
source_solid_id = self.face_solid_ids[face_id]
|
||||
tolerance = min(max(_shape_diagonal(self.shape) * 1e-7, 1e-6), 1e-3)
|
||||
visited = {face_id}
|
||||
queue = [face_id]
|
||||
while queue:
|
||||
current_id = queue.pop(0)
|
||||
for adjacent_id in self._adjacent_face_ids_for_edges(self._face_boundary_edge_ids(current_id), current_id):
|
||||
if adjacent_id in visited:
|
||||
continue
|
||||
if source_solid_id >= 0 and self.face_solid_ids[adjacent_id] != source_solid_id:
|
||||
continue
|
||||
candidate_surf = BRepAdaptor_Surface(self.faces[adjacent_id])
|
||||
if _surfaces_are_coplanar(source_surf, candidate_surf, tolerance):
|
||||
visited.add(adjacent_id)
|
||||
queue.append(adjacent_id)
|
||||
shared_edge_result = sorted(visited)
|
||||
if len(self.faces) > 600 or len(shared_edge_result) > 1:
|
||||
return shared_edge_result
|
||||
|
||||
interval_tolerance = max(tolerance * 20.0, _shape_diagonal(self.shape) * 1e-6, 1e-4)
|
||||
plane = source_surf.Plane()
|
||||
axis_point = plane.Location()
|
||||
@@ -2141,20 +2562,6 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
visited.add(candidate_id)
|
||||
queue.append(candidate_id)
|
||||
return sorted(visited)
|
||||
|
||||
visited = {face_id}
|
||||
queue = [face_id]
|
||||
while queue:
|
||||
current_id = queue.pop(0)
|
||||
for adjacent_id in self._adjacent_face_ids_for_edges(self._face_boundary_edge_ids(current_id), current_id):
|
||||
if adjacent_id in visited:
|
||||
continue
|
||||
if source_solid_id >= 0 and self.face_solid_ids[adjacent_id] != source_solid_id:
|
||||
continue
|
||||
candidate_surf = BRepAdaptor_Surface(self.faces[adjacent_id])
|
||||
if _surfaces_are_coplanar(source_surf, candidate_surf, tolerance):
|
||||
visited.add(adjacent_id)
|
||||
queue.append(adjacent_id)
|
||||
return sorted(visited)
|
||||
|
||||
def connected_same_domain_face_ids(self, face_id: int) -> list[int]:
|
||||
@@ -2189,9 +2596,25 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
radius = max(float(cylinder.Radius()), 0.0)
|
||||
diagonal = _shape_diagonal(self.shape)
|
||||
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
|
||||
visited = {face_id}
|
||||
queue = [face_id]
|
||||
while queue:
|
||||
current_id = queue.pop(0)
|
||||
for adjacent_id in self._adjacent_face_ids_for_edges(self._face_boundary_edge_ids(current_id), current_id):
|
||||
if adjacent_id in visited:
|
||||
continue
|
||||
if source_solid_id >= 0 and self.face_solid_ids[adjacent_id] != source_solid_id:
|
||||
continue
|
||||
candidate_surf = BRepAdaptor_Surface(self.faces[adjacent_id])
|
||||
if _surfaces_are_cocylindrical(source_surf, candidate_surf, tolerance):
|
||||
visited.add(adjacent_id)
|
||||
queue.append(adjacent_id)
|
||||
shared_edge_result = sorted(visited)
|
||||
if len(self.faces) > 600 or len(shared_edge_result) > 1:
|
||||
return shared_edge_result
|
||||
|
||||
interval_tolerance = max(tolerance * 50.0, diagonal * 1e-5, radius * 1e-4, 1e-3)
|
||||
source_interval = _shape_axis_interval(self.faces[face_id], axis_point, axis_dir)
|
||||
|
||||
candidates: dict[int, tuple[float, float]] = {}
|
||||
for candidate_id, face in enumerate(self.faces):
|
||||
if source_solid_id >= 0 and self.face_solid_ids[candidate_id] != source_solid_id:
|
||||
@@ -2220,20 +2643,6 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
visited.add(candidate_id)
|
||||
queue.append(candidate_id)
|
||||
return sorted(visited)
|
||||
|
||||
visited = {face_id}
|
||||
queue = [face_id]
|
||||
while queue:
|
||||
current_id = queue.pop(0)
|
||||
for adjacent_id in self._adjacent_face_ids_for_edges(self._face_boundary_edge_ids(current_id), current_id):
|
||||
if adjacent_id in visited:
|
||||
continue
|
||||
if source_solid_id >= 0 and self.face_solid_ids[adjacent_id] != source_solid_id:
|
||||
continue
|
||||
candidate_surf = BRepAdaptor_Surface(self.faces[adjacent_id])
|
||||
if _surfaces_are_cocylindrical(source_surf, candidate_surf, tolerance):
|
||||
visited.add(adjacent_id)
|
||||
queue.append(adjacent_id)
|
||||
return sorted(visited)
|
||||
|
||||
def _cylindrical_axis_range(
|
||||
|
||||
Reference in New Issue
Block a user