feat: 增强一级共面同轴事实识别
This commit is contained in:
@@ -289,7 +289,7 @@ STEP/B-Rep 参数化编辑主线
|
||||
│ ├── [x] [平行/垂直 -> 端面推拉一级关系守门]
|
||||
│ │ └── 移动端面策略会确认直接相邻平面和移动端面可验证,执行前给出平面约束摘要,执行后反查垂直/平行关系。
|
||||
│ ├── [x] [平行/垂直 -> Face/Edge 一级事实识别]
|
||||
│ │ └── Face 和 Edge 一级事实图会记录直接邻域内的平行、垂直、斜交关系数量和摘要,供 UI 说明、计划守门和后续约束求解复用。
|
||||
│ │ └── Face 和 Edge 一级事实图会记录直接邻域内的平行、垂直、斜交关系数量和摘要;Face 事实图还会明确同域/共面碎片和一级同轴圆柱关系,供 UI 说明、计划守门和后续约束求解复用。
|
||||
│ ├── [~] [平行/垂直 -> 通用可选约束]
|
||||
│ │ └── Face 偏移可走受限拉伸/切除,Face 面内长度/宽度的“保持关系”会转成所属对象单向缩放,Face 中心会转成所属对象平移,Edge 长度可走端面推拉;这些路径都会要求一级平面关系可验证并做结果反查。跨多面、跨特征的通用约束求解仍需继续泛化。
|
||||
│ ├── [x] [复杂曲线 Edge -> 可编辑入口守门]
|
||||
@@ -1352,7 +1352,7 @@ git diff --check
|
||||
- `scripts/verify_face_nonrectangular_local_edit.py`:临时生成三角柱 STEP,验证非矩形平面 Face 的 `局部重建`、`缩放特征`、`移动特征`、`拉伸/切除` 语义,在中心、面内尺寸和偏移场景下都能保持单 Solid;同时生成斜顶楔块,验证斜交一级相邻平面会提前阻止 `保持关系`。
|
||||
- `scripts/verify_face_resize_semantics.py`:加载 `cube_10mm.step`,验证 Face 的面内长度/面内宽度、中心坐标和偏移在 `当前面` 局部编辑、`整体` 所属对象编辑、`拉伸/切除` 与 `保持关系` 之间不会互相混淆;面积路径只作为后端缩放语义回归保留。
|
||||
- `scripts/verify_face_noop_guards.py`:验证 Face 的拉伸/切除、偏移、中心、面内尺寸和壳体厚度在目标值等于当前值时都会返回 blocked,并给出“不需要修改”的说明。
|
||||
- `scripts/verify_first_level_fact_graph.py`:验证正方体平面 Face、通孔圆柱面和槽/半孔候选都会输出统一一级拓扑事实包,包含主体 Face、边界 Edge/Vertex、共享边一级相邻 Face、角色分组,以及明确不自动传播的二级/三级关系;同时验证 Face 拉伸/切除/面积/中心移动、孔径修改和槽宽修改的编辑计划都会携带这份事实。
|
||||
- `scripts/verify_first_level_fact_graph.py`:验证正方体平面 Face、拆分共面 Face、空心圆柱端盖、通孔圆柱面和槽/半孔候选都会输出统一一级拓扑事实包,包含主体 Face、同域/共面碎片、一级同轴圆柱关系、边界 Edge/Vertex、共享边一级相邻 Face、角色分组,以及明确不自动传播的二级/三级关系;同时验证 Face 拉伸/切除/面积/中心移动、孔径修改和槽宽修改的编辑计划都会携带这份事实。
|
||||
- `scripts/verify_feature_recognition_summary.py`:验证平面 Face、带内孔平面 Face、通孔圆柱面、槽/半孔和凸台候选都会输出统一的 `识别摘要`、候选类型、常用优先级、置信度、风险、证据项、`当前可改` 和 `当前受限修改`;全局可编辑对象扫描也会按 Face、孔、槽、凸台、圆角/倒角、壳体、Edge 的常用顺序排序。
|
||||
- `scripts/verify_face_property_readback.py`:验证 Face 的面内尺寸、中心和偏移编辑完成后,当前选中对象表里的当前值和目标值会重新读到新几何;特别覆盖 `偏移` 的 `局部重建`、`移动特征` 和 `拉伸/切除` 三种语义。面积读回只作为诊断结果验证。
|
||||
- `scripts/verify_face_push_pull_limits.py`:验证平面 Face 向内切除时会估算当前面背后的材料厚度;浅范围切除允许执行,接近切穿会标为高风险,达到或超过材料厚度会直接 blocked;向外一次性拉伸/切除较大时会标为高风险并通过隔离子进程执行,而不是按绝对距离直接拒绝。
|
||||
|
||||
@@ -10,10 +10,13 @@ for path in (PROJECT_ROOT, SCRIPTS_DIR):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
from step_editor.geometry_utils import _int_values
|
||||
from step_editor.model import StepModel
|
||||
from step_editor.ui_helpers import INFO_LABELS
|
||||
from step_editor.window_state import WindowStateMixin
|
||||
|
||||
from verify_face_coplanar_push_pull import _top_faces, _write_split_top_box # noqa: E402
|
||||
from verify_face_mixed_surface_guard import _top_planar_cap_face, _write_hollow_cylinder # noqa: E402
|
||||
from verify_hole_resize import _first_hole_face, _write_through_hole_model # noqa: E402
|
||||
from verify_shell_thickness_resize import _first_shell_face, _write_plate_model # noqa: E402
|
||||
from verify_slot_resize import _first_slot_face, _write_half_round_slot_model # noqa: E402
|
||||
@@ -121,6 +124,19 @@ def _verify_planar_face_facts() -> None:
|
||||
"一级平面关系" in str(facts.get("first_level_fact_summary") or ""),
|
||||
f"cube planar Face fact summary should include planar relation facts: {facts}",
|
||||
)
|
||||
_assert(facts.get("first_level_same_domain_status") == "ready", f"cube same-domain status: {facts}")
|
||||
_assert(
|
||||
facts.get("first_level_same_domain_relation") == "coplanar",
|
||||
f"cube same-domain relation should be coplanar: {facts}",
|
||||
)
|
||||
_assert(
|
||||
int(facts.get("first_level_same_domain_face_count", 0) or 0) == 1,
|
||||
f"cube should expose a single same-domain planar Face: {facts}",
|
||||
)
|
||||
_assert(
|
||||
"first_level_coaxial_cylinder_summary" in facts,
|
||||
f"cube should still expose coaxial-cylinder fact fields: {facts}",
|
||||
)
|
||||
|
||||
probe = _FactProbe(model)
|
||||
fields = probe._face_first_level_selection_fields(face_id)
|
||||
@@ -154,6 +170,80 @@ def _verify_planar_face_facts() -> None:
|
||||
_assert_plan_facts(plan, label, "face")
|
||||
|
||||
|
||||
def _verify_coplanar_same_domain_facts(root: Path) -> None:
|
||||
split_path = root / "split_top_box.step"
|
||||
_write_split_top_box(split_path)
|
||||
model = StepModel.load(split_path)
|
||||
top_faces = _top_faces(model, 10.0, 2e-4)
|
||||
_assert(len(top_faces) == 2, f"split-top box should have two coplanar top Faces, got {top_faces}")
|
||||
|
||||
face_id = top_faces[0]
|
||||
facts = model.face_first_level_facts(face_id, scope="face")
|
||||
_assert_common_facts(facts, "split coplanar Face")
|
||||
_assert(
|
||||
facts.get("first_level_same_domain_status") == "ready",
|
||||
f"split coplanar Face should expose same-domain facts: {facts}",
|
||||
)
|
||||
_assert(
|
||||
facts.get("first_level_same_domain_relation") == "coplanar",
|
||||
f"split coplanar Face should be marked coplanar: {facts}",
|
||||
)
|
||||
_assert(
|
||||
set(_int_values(facts.get("first_level_same_domain_face_ids"))) == set(top_faces),
|
||||
f"split coplanar facts should include both top Faces: {facts}",
|
||||
)
|
||||
_assert(
|
||||
int(facts.get("first_level_same_domain_fragment_face_count", 0) or 0) == 1,
|
||||
f"split coplanar facts should expose one synchronized fragment: {facts}",
|
||||
)
|
||||
_assert(
|
||||
"共面" in str(facts.get("first_level_same_domain_summary") or ""),
|
||||
f"split coplanar summary should mention coplanar relation: {facts}",
|
||||
)
|
||||
|
||||
plan = model.push_pull_plan(face_id, 1.0)
|
||||
_assert_plan_facts(plan, "split coplanar push-pull plan", "face")
|
||||
_assert(
|
||||
set(_int_values(plan.get("first_level_same_domain_face_ids"))) == set(top_faces),
|
||||
f"split coplanar plan should carry same-domain facts: {plan}",
|
||||
)
|
||||
|
||||
|
||||
def _verify_coaxial_cylinder_facts(root: Path) -> None:
|
||||
hollow_path = root / "hollow_cylinder.step"
|
||||
_write_hollow_cylinder(hollow_path)
|
||||
model = StepModel.load(hollow_path)
|
||||
cap_face_id = _top_planar_cap_face(model)
|
||||
facts = model.face_first_level_facts(cap_face_id, scope="face")
|
||||
_assert_common_facts(facts, "hollow cylinder cap")
|
||||
_assert(
|
||||
facts.get("first_level_coaxial_cylinder_status") == "ready",
|
||||
f"hollow cylinder cap should expose ready coaxial-cylinder facts: {facts}",
|
||||
)
|
||||
_assert(
|
||||
int(facts.get("first_level_coaxial_cylinder_count", 0) or 0) >= 1,
|
||||
f"hollow cylinder cap should have at least one coaxial cylinder relation: {facts}",
|
||||
)
|
||||
_assert(
|
||||
int(facts.get("first_level_coaxial_cylinder_face_count", 0) or 0) >= 2,
|
||||
f"hollow cylinder cap should include inner and outer cylindrical Faces: {facts}",
|
||||
)
|
||||
rows = tuple(row for row in facts.get("first_level_coaxial_cylinder_rows", ()) or () if isinstance(row, dict))
|
||||
radii = sorted({round(float(radius), 4) for row in rows for radius in tuple(row.get("radii") or ())})
|
||||
_assert(radii == [3.0, 8.0], f"hollow cylinder cap should expose inner/outer coaxial radii, got {radii}: {facts}")
|
||||
_assert(
|
||||
"同轴圆柱" in str(facts.get("first_level_coaxial_cylinder_summary") or ""),
|
||||
f"hollow cylinder cap summary should mention coaxial cylinders: {facts}",
|
||||
)
|
||||
|
||||
plan = model.push_pull_plan(cap_face_id, 1.0)
|
||||
_assert_plan_facts(plan, "hollow cylinder cap push-pull plan", "face")
|
||||
_assert(
|
||||
plan.get("first_level_coaxial_cylinder_status") == "ready",
|
||||
f"hollow cylinder cap plan should carry coaxial-cylinder facts: {plan}",
|
||||
)
|
||||
|
||||
|
||||
def _verify_shell_plan_facts(root: Path) -> None:
|
||||
shell_path = root / "thin_plate.step"
|
||||
_write_plate_model(shell_path)
|
||||
@@ -217,12 +307,16 @@ def main() -> int:
|
||||
"first_level_fact_summary",
|
||||
"first_level_fact_subject_face_count",
|
||||
"first_level_fact_adjacent_face_count",
|
||||
"first_level_same_domain_summary",
|
||||
"first_level_coaxial_cylinder_summary",
|
||||
):
|
||||
_assert(key in INFO_LABELS, f"{key} should have a user-facing label")
|
||||
|
||||
_verify_planar_face_facts()
|
||||
with tempfile.TemporaryDirectory(prefix="geom_param_first_level_facts_") as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
_verify_coplanar_same_domain_facts(root)
|
||||
_verify_coaxial_cylinder_facts(root)
|
||||
_verify_shell_plan_facts(root)
|
||||
_verify_cylindrical_facts(root)
|
||||
_verify_slot_facts(root)
|
||||
|
||||
@@ -3299,6 +3299,152 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
),
|
||||
}
|
||||
|
||||
def _first_level_same_domain_relation_fields(
|
||||
self,
|
||||
*,
|
||||
face_id: int,
|
||||
same_domain_face_ids: Iterable[int],
|
||||
relation_scope: str = "face",
|
||||
) -> dict[str, object]:
|
||||
domain_ids = tuple(sorted({int(item) for item in same_domain_face_ids if 0 <= int(item) < len(self.faces)}))
|
||||
surface = self.face_surface_kind(face_id) if 0 <= int(face_id) < len(self.faces) else "unknown"
|
||||
fragment_ids = tuple(item for item in domain_ids if item != int(face_id))
|
||||
if surface == "plane":
|
||||
relation = "coplanar"
|
||||
relation_label = "共面"
|
||||
elif surface == "cylinder":
|
||||
relation = "cocylindrical"
|
||||
relation_label = "同圆柱面"
|
||||
elif len(domain_ids) > 1:
|
||||
relation = "same-domain"
|
||||
relation_label = "同域"
|
||||
else:
|
||||
relation = "single-face"
|
||||
relation_label = "单 Face"
|
||||
|
||||
if len(domain_ids) > 1:
|
||||
summary = f"一级同域关系:已识别 {len(domain_ids)} 个{relation_label} Face,会作为同一局部主体。"
|
||||
elif surface in {"plane", "cylinder"}:
|
||||
summary = f"一级同域关系:当前为单个{relation_label}主体,未发现可同步碎片 Face。"
|
||||
else:
|
||||
summary = "一级同域关系:当前曲面类型暂不支持同域碎片同步,只按单 Face 处理。"
|
||||
|
||||
return {
|
||||
"first_level_same_domain_status": "ready",
|
||||
"first_level_same_domain_scope": relation_scope,
|
||||
"first_level_same_domain_relation": relation,
|
||||
"first_level_same_domain_relation_label": relation_label,
|
||||
"first_level_same_domain_surface": surface,
|
||||
"first_level_same_domain_face_ids": domain_ids,
|
||||
"first_level_same_domain_face_count": len(domain_ids),
|
||||
"first_level_same_domain_fragment_face_ids": fragment_ids,
|
||||
"first_level_same_domain_fragment_face_count": len(fragment_ids),
|
||||
"first_level_same_domain_summary": summary,
|
||||
"first_level_same_domain_note": (
|
||||
"同域/共面/同圆柱面碎片属于当前一级主体;当前只同步明确支持的编辑路径,"
|
||||
"不会递归扩展到二级、三级邻域。"
|
||||
),
|
||||
}
|
||||
|
||||
def _cylinders_are_coaxial(
|
||||
self,
|
||||
left: BRepAdaptor_Surface,
|
||||
right: BRepAdaptor_Surface,
|
||||
tolerance: float,
|
||||
) -> bool:
|
||||
if left.GetType() != GeomAbs_Cylinder or right.GetType() != GeomAbs_Cylinder:
|
||||
return False
|
||||
left_axis = left.Cylinder().Axis()
|
||||
right_axis = right.Cylinder().Axis()
|
||||
axis_dot = abs(_direction_dot(left_axis.Direction(), right_axis.Direction()))
|
||||
if axis_dot < 1.0 - 1e-6:
|
||||
return False
|
||||
distance = _point_axis_distance(left_axis.Location(), left_axis.Direction(), right_axis.Location())
|
||||
return distance <= max(tolerance, _shape_diagonal(self.shape) * 1e-7, 1e-6)
|
||||
|
||||
def _first_level_coaxial_cylinder_relation_fields(
|
||||
self,
|
||||
*,
|
||||
subject_face_ids: Iterable[int],
|
||||
adjacent_face_ids: Iterable[int],
|
||||
relation_scope: str = "face",
|
||||
) -> dict[str, object]:
|
||||
involved_ids = tuple(
|
||||
sorted({int(item) for item in (*tuple(subject_face_ids), *tuple(adjacent_face_ids)) if 0 <= int(item) < len(self.faces)})
|
||||
)
|
||||
diagonal = _shape_diagonal(self.shape)
|
||||
tolerance = min(max(diagonal * 1e-7, 1e-6), 1e-3)
|
||||
cylinder_infos: dict[int, dict[str, object]] = {}
|
||||
for face_id in involved_ids:
|
||||
surf = BRepAdaptor_Surface(self.faces[face_id])
|
||||
if surf.GetType() != GeomAbs_Cylinder:
|
||||
continue
|
||||
cylinder = surf.Cylinder()
|
||||
axis = cylinder.Axis()
|
||||
cylinder_infos[face_id] = {
|
||||
"face_id": face_id,
|
||||
"axis_point": _point_tuple(axis.Location()),
|
||||
"axis_direction": _dir_tuple(axis.Direction()),
|
||||
"radius": float(cylinder.Radius()),
|
||||
}
|
||||
|
||||
rows: list[dict[str, object]] = []
|
||||
cylinder_ids = tuple(sorted(cylinder_infos))
|
||||
for left_index, left_id in enumerate(cylinder_ids):
|
||||
left_surf = BRepAdaptor_Surface(self.faces[left_id])
|
||||
for right_id in cylinder_ids[left_index + 1 :]:
|
||||
right_surf = BRepAdaptor_Surface(self.faces[right_id])
|
||||
if not self._cylinders_are_coaxial(left_surf, right_surf, tolerance):
|
||||
continue
|
||||
left_radius = float(cylinder_infos[left_id]["radius"])
|
||||
right_radius = float(cylinder_infos[right_id]["radius"])
|
||||
rows.append(
|
||||
{
|
||||
"relation_kind": "cylinder-cylinder-axis",
|
||||
"source": "first-level-cylinder-pair",
|
||||
"face_ids": (left_id, right_id),
|
||||
"relation": "coaxial",
|
||||
"relation_label": "同轴",
|
||||
"radii": (left_radius, right_radius),
|
||||
"same_radius": abs(left_radius - right_radius) <= max(tolerance, max(left_radius, right_radius) * 1e-6),
|
||||
}
|
||||
)
|
||||
|
||||
related_face_ids = tuple(sorted({face_id for row in rows for face_id in _int_values(row.get("face_ids"))}))
|
||||
if rows:
|
||||
unique_radii = []
|
||||
for face_id in related_face_ids:
|
||||
radius = float(cylinder_infos[face_id]["radius"])
|
||||
if not any(abs(radius - existing) <= max(tolerance, max(radius, existing) * 1e-6) for existing in unique_radii):
|
||||
unique_radii.append(radius)
|
||||
summary = (
|
||||
f"一级同轴圆柱关系:已识别 {len(rows)} 组同轴圆柱 Face;"
|
||||
f"相关 Face {len(related_face_ids)},半径 {tuple(round(item, 6) for item in sorted(unique_radii))}。"
|
||||
)
|
||||
status = "ready"
|
||||
elif cylinder_infos:
|
||||
summary = "一级同轴圆柱关系:一级范围内只有单个圆柱 Face,暂时没有可配对同轴关系。"
|
||||
status = "unavailable"
|
||||
else:
|
||||
summary = "一级同轴圆柱关系:一级范围内没有圆柱 Face。"
|
||||
status = "unavailable"
|
||||
|
||||
return {
|
||||
"first_level_coaxial_cylinder_status": status,
|
||||
"first_level_coaxial_cylinder_scope": relation_scope,
|
||||
"first_level_coaxial_cylinder_rows": tuple(rows),
|
||||
"first_level_coaxial_cylinder_count": len(rows),
|
||||
"first_level_coaxial_cylinder_face_ids": related_face_ids,
|
||||
"first_level_coaxial_cylinder_face_count": len(related_face_ids),
|
||||
"first_level_coaxial_cylinder_candidate_face_ids": cylinder_ids,
|
||||
"first_level_coaxial_cylinder_candidate_face_count": len(cylinder_ids),
|
||||
"first_level_coaxial_cylinder_summary": summary,
|
||||
"first_level_coaxial_cylinder_note": (
|
||||
"这些是一级范围内圆柱轴线重合的事实;当前只作为识别和守门证据,"
|
||||
"跨特征同轴传播仍不自动执行。"
|
||||
),
|
||||
}
|
||||
|
||||
def face_first_level_topology(self, face_id: int) -> dict[str, object]:
|
||||
"""Return the explicit first-level B-Rep neighborhood for a Face.
|
||||
|
||||
@@ -3367,6 +3513,16 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
adjacent_face_ids=adjacent_face_ids,
|
||||
relation_scope="face",
|
||||
)
|
||||
same_domain_relation_fields = self._first_level_same_domain_relation_fields(
|
||||
face_id=face_id,
|
||||
same_domain_face_ids=same_domain_face_ids,
|
||||
relation_scope="face",
|
||||
)
|
||||
coaxial_cylinder_relation_fields = self._first_level_coaxial_cylinder_relation_fields(
|
||||
subject_face_ids=same_domain_face_ids,
|
||||
adjacent_face_ids=adjacent_face_ids,
|
||||
relation_scope="face",
|
||||
)
|
||||
topology = {
|
||||
"topology_relation_model": "STEP/B-Rep shared-edge first-level",
|
||||
"topology_relation_depth": 1,
|
||||
@@ -3395,6 +3551,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"first_level_face_ids": first_level_face_ids,
|
||||
"first_level_face_count": len(first_level_face_ids),
|
||||
**planar_relation_fields,
|
||||
**same_domain_relation_fields,
|
||||
**coaxial_cylinder_relation_fields,
|
||||
"first_level_topology_note": (
|
||||
f"已识别当前 Face 区域 {len(same_domain_face_ids)} 个 Face、"
|
||||
f"边界 Edge {len(region_boundary_edge_ids)} 条、"
|
||||
@@ -3498,6 +3656,16 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
for adjacent_id, edge_ids in sorted(shared_edges_by_face.items())
|
||||
)
|
||||
|
||||
same_domain_relation_fields = self._first_level_same_domain_relation_fields(
|
||||
face_id=face_id,
|
||||
same_domain_face_ids=side_face_ids,
|
||||
relation_scope="cylindrical-feature",
|
||||
)
|
||||
coaxial_cylinder_relation_fields = self._first_level_coaxial_cylinder_relation_fields(
|
||||
subject_face_ids=side_face_ids,
|
||||
adjacent_face_ids=adjacent_face_ids,
|
||||
relation_scope="cylindrical-feature",
|
||||
)
|
||||
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",
|
||||
@@ -3540,6 +3708,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"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),
|
||||
**same_domain_relation_fields,
|
||||
**coaxial_cylinder_relation_fields,
|
||||
"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)}. "
|
||||
@@ -3647,6 +3817,16 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
for key, value in topology.items()
|
||||
if str(key).startswith("first_level_planar_relation_")
|
||||
}
|
||||
same_domain_relation_fields = {
|
||||
key: value
|
||||
for key, value in topology.items()
|
||||
if str(key).startswith("first_level_same_domain_")
|
||||
}
|
||||
coaxial_cylinder_relation_fields = {
|
||||
key: value
|
||||
for key, value in topology.items()
|
||||
if str(key).startswith("first_level_coaxial_cylinder_")
|
||||
}
|
||||
if not planar_relation_fields:
|
||||
planar_relation_fields = self._first_level_planar_relation_fields(
|
||||
subject_face_ids=subject_face_ids,
|
||||
@@ -3654,13 +3834,21 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
relation_scope=resolved_scope,
|
||||
)
|
||||
planar_summary = str(planar_relation_fields.get("first_level_planar_relation_summary") or "").strip()
|
||||
same_domain_summary = str(same_domain_relation_fields.get("first_level_same_domain_summary") or "").strip()
|
||||
coaxial_summary = str(
|
||||
coaxial_cylinder_relation_fields.get("first_level_coaxial_cylinder_summary") or ""
|
||||
).strip()
|
||||
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."
|
||||
)
|
||||
if same_domain_summary and int(same_domain_relation_fields.get("first_level_same_domain_face_count", 0) or 0) > 1:
|
||||
summary = f"{summary} {same_domain_summary}"
|
||||
if planar_summary and planar_relation_fields.get("first_level_planar_relation_status") == "ready":
|
||||
summary = f"{summary} {planar_summary}"
|
||||
if coaxial_summary and coaxial_cylinder_relation_fields.get("first_level_coaxial_cylinder_status") == "ready":
|
||||
summary = f"{summary} {coaxial_summary}"
|
||||
facts = {
|
||||
"first_level_fact_model": "STEP/B-Rep first-level fact graph",
|
||||
"first_level_fact_source_model": source_model,
|
||||
@@ -3686,6 +3874,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"first_level_fact_ignored_relation_note": topology.get("topology_ignored_relation_note", ""),
|
||||
"first_level_fact_summary": summary,
|
||||
**planar_relation_fields,
|
||||
**same_domain_relation_fields,
|
||||
**coaxial_cylinder_relation_fields,
|
||||
}
|
||||
for item in facts["first_level_fact_subject_face_ids"]:
|
||||
self._face_first_level_fact_cache[(int(item), resolved_scope)] = dict(facts)
|
||||
|
||||
@@ -91,6 +91,27 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
|
||||
"first_level_planar_relation_observed_modes",
|
||||
"first_level_planar_relation_summary",
|
||||
"first_level_planar_relation_note",
|
||||
"first_level_same_domain_status",
|
||||
"first_level_same_domain_scope",
|
||||
"first_level_same_domain_relation",
|
||||
"first_level_same_domain_relation_label",
|
||||
"first_level_same_domain_surface",
|
||||
"first_level_same_domain_face_ids",
|
||||
"first_level_same_domain_face_count",
|
||||
"first_level_same_domain_fragment_face_ids",
|
||||
"first_level_same_domain_fragment_face_count",
|
||||
"first_level_same_domain_summary",
|
||||
"first_level_same_domain_note",
|
||||
"first_level_coaxial_cylinder_status",
|
||||
"first_level_coaxial_cylinder_scope",
|
||||
"first_level_coaxial_cylinder_count",
|
||||
"first_level_coaxial_cylinder_face_ids",
|
||||
"first_level_coaxial_cylinder_face_count",
|
||||
"first_level_coaxial_cylinder_candidate_face_ids",
|
||||
"first_level_coaxial_cylinder_candidate_face_count",
|
||||
"first_level_coaxial_cylinder_rows",
|
||||
"first_level_coaxial_cylinder_summary",
|
||||
"first_level_coaxial_cylinder_note",
|
||||
"face_push_pull_planar_relation_constraint_requested",
|
||||
"face_push_pull_planar_relation_constraint_label",
|
||||
"face_push_pull_planar_constraint_status",
|
||||
@@ -579,6 +600,27 @@ INFO_LABELS = {
|
||||
"first_level_planar_relation_observed_modes": "已识别平面关系",
|
||||
"first_level_planar_relation_summary": "一级平面关系摘要",
|
||||
"first_level_planar_relation_note": "一级平面关系说明",
|
||||
"first_level_same_domain_status": "一级同域状态",
|
||||
"first_level_same_domain_scope": "一级同域范围",
|
||||
"first_level_same_domain_relation": "一级同域关系",
|
||||
"first_level_same_domain_relation_label": "一级同域关系名称",
|
||||
"first_level_same_domain_surface": "一级同域曲面",
|
||||
"first_level_same_domain_face_ids": "一级同域 Face",
|
||||
"first_level_same_domain_face_count": "一级同域 Face 数",
|
||||
"first_level_same_domain_fragment_face_ids": "一级同域碎片 Face",
|
||||
"first_level_same_domain_fragment_face_count": "一级同域碎片数",
|
||||
"first_level_same_domain_summary": "一级同域摘要",
|
||||
"first_level_same_domain_note": "一级同域说明",
|
||||
"first_level_coaxial_cylinder_status": "一级同轴圆柱状态",
|
||||
"first_level_coaxial_cylinder_scope": "一级同轴圆柱范围",
|
||||
"first_level_coaxial_cylinder_count": "一级同轴圆柱关系数",
|
||||
"first_level_coaxial_cylinder_face_ids": "一级同轴圆柱 Face",
|
||||
"first_level_coaxial_cylinder_face_count": "一级同轴圆柱 Face 数",
|
||||
"first_level_coaxial_cylinder_candidate_face_ids": "一级圆柱候选 Face",
|
||||
"first_level_coaxial_cylinder_candidate_face_count": "一级圆柱候选数",
|
||||
"first_level_coaxial_cylinder_rows": "一级同轴圆柱关系列表",
|
||||
"first_level_coaxial_cylinder_summary": "一级同轴圆柱摘要",
|
||||
"first_level_coaxial_cylinder_note": "一级同轴圆柱说明",
|
||||
"face_push_pull_planar_relation_constraint_requested": "Face保持关系已请求",
|
||||
"face_push_pull_planar_relation_constraint_label": "Face保持关系约束",
|
||||
"face_push_pull_planar_constraint_status": "Face保持关系状态",
|
||||
|
||||
@@ -1553,6 +1553,7 @@ class WindowStateMixin:
|
||||
"凸台:圆柱凸台直径/高度/轴心;矩形凸台/矩形槽口袋长宽、中心、高度/深度;多台阶矩形凸台顶层规则台阶。\n"
|
||||
"圆角/倒角:简单已有圆角半径/弧长、简单等半径圆角链半径/弧长、已有等距倒角距离,直线 Edge 新增圆角/倒角。\n"
|
||||
"Edge/解析曲面:直线 Edge 长度/端点/中心、圆/椭圆 Edge、简单圆锥/球/环面。\n"
|
||||
"一级关系:Face/Edge 平行垂直事实、Face 共面/同域碎片、一级同轴圆柱事实;部分 Face/Edge 操作可选择保持关系。\n"
|
||||
"受限:复杂链式特征、二级/三级拓扑传播和原 CAD 历史恢复。"
|
||||
)
|
||||
return headline, detail, tooltip
|
||||
|
||||
Reference in New Issue
Block a user