feat: 增强一级共面同轴事实识别

This commit is contained in:
2026-08-10 18:05:18 +08:00
parent 3a851d0967
commit 3aadd6a34e
5 changed files with 329 additions and 2 deletions
+2 -2
View File
@@ -289,7 +289,7 @@ STEP/B-Rep 参数化编辑主线
│ ├── [x] [平行/垂直 -> 端面推拉一级关系守门] │ ├── [x] [平行/垂直 -> 端面推拉一级关系守门]
│ │ └── 移动端面策略会确认直接相邻平面和移动端面可验证,执行前给出平面约束摘要,执行后反查垂直/平行关系。 │ │ └── 移动端面策略会确认直接相邻平面和移动端面可验证,执行前给出平面约束摘要,执行后反查垂直/平行关系。
│ ├── [x] [平行/垂直 -> Face/Edge 一级事实识别] │ ├── [x] [平行/垂直 -> Face/Edge 一级事实识别]
│ │ └── Face 和 Edge 一级事实图会记录直接邻域内的平行、垂直、斜交关系数量和摘要,供 UI 说明、计划守门和后续约束求解复用。 │ │ └── Face 和 Edge 一级事实图会记录直接邻域内的平行、垂直、斜交关系数量和摘要;Face 事实图还会明确同域/共面碎片和一级同轴圆柱关系,供 UI 说明、计划守门和后续约束求解复用。
│ ├── [~] [平行/垂直 -> 通用可选约束] │ ├── [~] [平行/垂直 -> 通用可选约束]
│ │ └── Face 偏移可走受限拉伸/切除,Face 面内长度/宽度的“保持关系”会转成所属对象单向缩放,Face 中心会转成所属对象平移,Edge 长度可走端面推拉;这些路径都会要求一级平面关系可验证并做结果反查。跨多面、跨特征的通用约束求解仍需继续泛化。 │ │ └── Face 偏移可走受限拉伸/切除,Face 面内长度/宽度的“保持关系”会转成所属对象单向缩放,Face 中心会转成所属对象平移,Edge 长度可走端面推拉;这些路径都会要求一级平面关系可验证并做结果反查。跨多面、跨特征的通用约束求解仍需继续泛化。
│ ├── [x] [复杂曲线 Edge -> 可编辑入口守门] │ ├── [x] [复杂曲线 Edge -> 可编辑入口守门]
@@ -1352,7 +1352,7 @@ git diff --check
- `scripts/verify_face_nonrectangular_local_edit.py`:临时生成三角柱 STEP,验证非矩形平面 Face 的 `局部重建``缩放特征``移动特征``拉伸/切除` 语义,在中心、面内尺寸和偏移场景下都能保持单 Solid;同时生成斜顶楔块,验证斜交一级相邻平面会提前阻止 `保持关系` - `scripts/verify_face_nonrectangular_local_edit.py`:临时生成三角柱 STEP,验证非矩形平面 Face 的 `局部重建``缩放特征``移动特征``拉伸/切除` 语义,在中心、面内尺寸和偏移场景下都能保持单 Solid;同时生成斜顶楔块,验证斜交一级相邻平面会提前阻止 `保持关系`
- `scripts/verify_face_resize_semantics.py`:加载 `cube_10mm.step`,验证 Face 的面内长度/面内宽度、中心坐标和偏移在 `当前面` 局部编辑、`整体` 所属对象编辑、`拉伸/切除``保持关系` 之间不会互相混淆;面积路径只作为后端缩放语义回归保留。 - `scripts/verify_face_resize_semantics.py`:加载 `cube_10mm.step`,验证 Face 的面内长度/面内宽度、中心坐标和偏移在 `当前面` 局部编辑、`整体` 所属对象编辑、`拉伸/切除``保持关系` 之间不会互相混淆;面积路径只作为后端缩放语义回归保留。
- `scripts/verify_face_noop_guards.py`:验证 Face 的拉伸/切除、偏移、中心、面内尺寸和壳体厚度在目标值等于当前值时都会返回 blocked,并给出“不需要修改”的说明。 - `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_feature_recognition_summary.py`:验证平面 Face、带内孔平面 Face、通孔圆柱面、槽/半孔和凸台候选都会输出统一的 `识别摘要`、候选类型、常用优先级、置信度、风险、证据项、`当前可改``当前受限修改`;全局可编辑对象扫描也会按 Face、孔、槽、凸台、圆角/倒角、壳体、Edge 的常用顺序排序。
- `scripts/verify_face_property_readback.py`:验证 Face 的面内尺寸、中心和偏移编辑完成后,当前选中对象表里的当前值和目标值会重新读到新几何;特别覆盖 `偏移``局部重建``移动特征``拉伸/切除` 三种语义。面积读回只作为诊断结果验证。 - `scripts/verify_face_property_readback.py`:验证 Face 的面内尺寸、中心和偏移编辑完成后,当前选中对象表里的当前值和目标值会重新读到新几何;特别覆盖 `偏移``局部重建``移动特征``拉伸/切除` 三种语义。面积读回只作为诊断结果验证。
- `scripts/verify_face_push_pull_limits.py`:验证平面 Face 向内切除时会估算当前面背后的材料厚度;浅范围切除允许执行,接近切穿会标为高风险,达到或超过材料厚度会直接 blocked;向外一次性拉伸/切除较大时会标为高风险并通过隔离子进程执行,而不是按绝对距离直接拒绝。 - `scripts/verify_face_push_pull_limits.py`:验证平面 Face 向内切除时会估算当前面背后的材料厚度;浅范围切除允许执行,接近切穿会标为高风险,达到或超过材料厚度会直接 blocked;向外一次性拉伸/切除较大时会标为高风险并通过隔离子进程执行,而不是按绝对距离直接拒绝。
+94
View File
@@ -10,10 +10,13 @@ for path in (PROJECT_ROOT, SCRIPTS_DIR):
if str(path) not in sys.path: if str(path) not in sys.path:
sys.path.insert(0, str(path)) sys.path.insert(0, str(path))
from step_editor.geometry_utils import _int_values
from step_editor.model import StepModel from step_editor.model import StepModel
from step_editor.ui_helpers import INFO_LABELS from step_editor.ui_helpers import INFO_LABELS
from step_editor.window_state import WindowStateMixin 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_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_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 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 ""), "一级平面关系" in str(facts.get("first_level_fact_summary") or ""),
f"cube planar Face fact summary should include planar relation facts: {facts}", 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) probe = _FactProbe(model)
fields = probe._face_first_level_selection_fields(face_id) 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") _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: def _verify_shell_plan_facts(root: Path) -> None:
shell_path = root / "thin_plate.step" shell_path = root / "thin_plate.step"
_write_plate_model(shell_path) _write_plate_model(shell_path)
@@ -217,12 +307,16 @@ def main() -> int:
"first_level_fact_summary", "first_level_fact_summary",
"first_level_fact_subject_face_count", "first_level_fact_subject_face_count",
"first_level_fact_adjacent_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") _assert(key in INFO_LABELS, f"{key} should have a user-facing label")
_verify_planar_face_facts() _verify_planar_face_facts()
with tempfile.TemporaryDirectory(prefix="geom_param_first_level_facts_") as temp_dir: with tempfile.TemporaryDirectory(prefix="geom_param_first_level_facts_") as temp_dir:
root = Path(temp_dir) root = Path(temp_dir)
_verify_coplanar_same_domain_facts(root)
_verify_coaxial_cylinder_facts(root)
_verify_shell_plan_facts(root) _verify_shell_plan_facts(root)
_verify_cylindrical_facts(root) _verify_cylindrical_facts(root)
_verify_slot_facts(root) _verify_slot_facts(root)
+190
View File
@@ -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]: def face_first_level_topology(self, face_id: int) -> dict[str, object]:
"""Return the explicit first-level B-Rep neighborhood for a Face. """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, adjacent_face_ids=adjacent_face_ids,
relation_scope="face", 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 = {
"topology_relation_model": "STEP/B-Rep shared-edge first-level", "topology_relation_model": "STEP/B-Rep shared-edge first-level",
"topology_relation_depth": 1, "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_ids": first_level_face_ids,
"first_level_face_count": len(first_level_face_ids), "first_level_face_count": len(first_level_face_ids),
**planar_relation_fields, **planar_relation_fields,
**same_domain_relation_fields,
**coaxial_cylinder_relation_fields,
"first_level_topology_note": ( "first_level_topology_note": (
f"已识别当前 Face 区域 {len(same_domain_face_ids)} 个 Face、" f"已识别当前 Face 区域 {len(same_domain_face_ids)} 个 Face、"
f"边界 Edge {len(region_boundary_edge_ids)} 条、" 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()) 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"))) angular_span = feature.get("slot_angular_span", feature.get("angular_span", info.get("angular_span")))
topology = { topology = {
"topology_relation_model": "STEP/B-Rep cylindrical-feature shared-edge first-level", "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_opening_face_count": len(opening_face_ids),
"cylindrical_feature_slot_boundary_face_ids": slot_boundary_face_ids, "cylindrical_feature_slot_boundary_face_ids": slot_boundary_face_ids,
"cylindrical_feature_slot_boundary_face_count": len(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": ( "first_level_topology_note": (
f"Cylindrical side Faces={len(side_face_ids)}, boundary Edges={len(region_boundary_edge_ids)}, " 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)}. " 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() for key, value in topology.items()
if str(key).startswith("first_level_planar_relation_") 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: if not planar_relation_fields:
planar_relation_fields = self._first_level_planar_relation_fields( planar_relation_fields = self._first_level_planar_relation_fields(
subject_face_ids=subject_face_ids, subject_face_ids=subject_face_ids,
@@ -3654,13 +3834,21 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
relation_scope=resolved_scope, relation_scope=resolved_scope,
) )
planar_summary = str(planar_relation_fields.get("first_level_planar_relation_summary") or "").strip() 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 = ( summary = (
f"{subject_role}: Face {len(subject_face_ids)}, boundary Edge {len(boundary_edge_ids)}, " 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)}; " 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." "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": if planar_summary and planar_relation_fields.get("first_level_planar_relation_status") == "ready":
summary = f"{summary} {planar_summary}" 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 = { facts = {
"first_level_fact_model": "STEP/B-Rep first-level fact graph", "first_level_fact_model": "STEP/B-Rep first-level fact graph",
"first_level_fact_source_model": source_model, "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_ignored_relation_note": topology.get("topology_ignored_relation_note", ""),
"first_level_fact_summary": summary, "first_level_fact_summary": summary,
**planar_relation_fields, **planar_relation_fields,
**same_domain_relation_fields,
**coaxial_cylinder_relation_fields,
} }
for item in facts["first_level_fact_subject_face_ids"]: 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[(int(item), resolved_scope)] = dict(facts)
+42
View File
@@ -91,6 +91,27 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
"first_level_planar_relation_observed_modes", "first_level_planar_relation_observed_modes",
"first_level_planar_relation_summary", "first_level_planar_relation_summary",
"first_level_planar_relation_note", "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_requested",
"face_push_pull_planar_relation_constraint_label", "face_push_pull_planar_relation_constraint_label",
"face_push_pull_planar_constraint_status", "face_push_pull_planar_constraint_status",
@@ -579,6 +600,27 @@ INFO_LABELS = {
"first_level_planar_relation_observed_modes": "已识别平面关系", "first_level_planar_relation_observed_modes": "已识别平面关系",
"first_level_planar_relation_summary": "一级平面关系摘要", "first_level_planar_relation_summary": "一级平面关系摘要",
"first_level_planar_relation_note": "一级平面关系说明", "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_requested": "Face保持关系已请求",
"face_push_pull_planar_relation_constraint_label": "Face保持关系约束", "face_push_pull_planar_relation_constraint_label": "Face保持关系约束",
"face_push_pull_planar_constraint_status": "Face保持关系状态", "face_push_pull_planar_constraint_status": "Face保持关系状态",
+1
View File
@@ -1553,6 +1553,7 @@ class WindowStateMixin:
"凸台:圆柱凸台直径/高度/轴心;矩形凸台/矩形槽口袋长宽、中心、高度/深度;多台阶矩形凸台顶层规则台阶。\n" "凸台:圆柱凸台直径/高度/轴心;矩形凸台/矩形槽口袋长宽、中心、高度/深度;多台阶矩形凸台顶层规则台阶。\n"
"圆角/倒角:简单已有圆角半径/弧长、简单等半径圆角链半径/弧长、已有等距倒角距离,直线 Edge 新增圆角/倒角。\n" "圆角/倒角:简单已有圆角半径/弧长、简单等半径圆角链半径/弧长、已有等距倒角距离,直线 Edge 新增圆角/倒角。\n"
"Edge/解析曲面:直线 Edge 长度/端点/中心、圆/椭圆 Edge、简单圆锥/球/环面。\n" "Edge/解析曲面:直线 Edge 长度/端点/中心、圆/椭圆 Edge、简单圆锥/球/环面。\n"
"一级关系:Face/Edge 平行垂直事实、Face 共面/同域碎片、一级同轴圆柱事实;部分 Face/Edge 操作可选择保持关系。\n"
"受限:复杂链式特征、二级/三级拓扑传播和原 CAD 历史恢复。" "受限:复杂链式特征、二级/三级拓扑传播和原 CAD 历史恢复。"
) )
return headline, detail, tooltip return headline, detail, tooltip