feat: 完善Face保持关系守门与大模型预判

This commit is contained in:
2026-08-10 16:51:52 +08:00
parent 415e8a0f27
commit 61f0b76e59
11 changed files with 218 additions and 21 deletions
+4
View File
@@ -58,6 +58,10 @@ CASES: tuple[tuple[str, tuple[str, ...]], ...] = (
"face offset, push pull",
("verify_face_resize_semantics.py", "--property", "offset", "--strategy", "push_pull", "--offset-distance", "1"),
),
(
"face offset, keep first-level planar relations",
("verify_face_resize_semantics.py", "--property", "offset", "--strategy", "keep_relations", "--offset-distance", "1"),
),
(
"face offset, push pull inward cut",
("verify_face_resize_semantics.py", "--property", "offset", "--strategy", "push_pull", "--offset-distance", "-1"),
@@ -15,12 +15,12 @@ from scripts.verify_large_stepped_cap_push_pull import _large_multi_boundary_cap
MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "geom_extract.step"
FACE_ID = 594
BASE_FACE_KEYS = (
"area",
"local_face_width",
"local_face_height",
"face_center_position",
"face_target_normal_position",
)
RESULT_ONLY_KEYS = ("area",)
class _Probe(WindowStateMixin):
@@ -71,6 +71,12 @@ def _assert_contains(keys: tuple[str, ...], expected: tuple[str, ...], label: st
raise AssertionError(f"{label} missing {missing}, got {keys}")
def _assert_absent(keys: tuple[str, ...], forbidden: tuple[str, ...], label: str) -> None:
leaked = [key for key in forbidden if key in keys]
if leaked:
raise AssertionError(f"{label} should keep result-only values out of feature parameters: {leaked}")
def _assert_face_594_stays_stable_after_remote_cap_edit() -> None:
model = StepModel.load(MODEL_PATH)
source_face_id = _large_stepped_cap_face(model)
@@ -123,16 +129,19 @@ def main() -> int:
before_cached_rows = _feature_rows(model, FACE_ID)
_assert_contains(before_cached_rows, BASE_FACE_KEYS, "Face 594 before full feature cache")
_assert_absent(before_cached_rows, RESULT_ONLY_KEYS, "Face 594 before full feature cache")
full_info = model.feature_info(FACE_ID)
if full_info.get("shell_region_status") == "candidate":
dimension_keys = _feature_dimension_keys(full_info)
_assert_contains(dimension_keys, BASE_FACE_KEYS, "shell candidate feature dimensions")
_assert_absent(dimension_keys, RESULT_ONLY_KEYS, "shell candidate feature dimensions")
if "shell_thickness_estimate" not in dimension_keys:
raise AssertionError(f"shell candidate should keep shell thickness as an extra dimension: {dimension_keys}")
after_cached_rows = _feature_rows(model, FACE_ID)
_assert_contains(after_cached_rows, BASE_FACE_KEYS, "Face 594 after full feature cache")
_assert_absent(after_cached_rows, RESULT_ONLY_KEYS, "Face 594 after full feature cache")
if before_cached_rows != after_cached_rows:
raise AssertionError(
"Face 594 current-only feature rows changed after full recognition cache: "
+8 -2
View File
@@ -144,12 +144,18 @@ def _assert_selection_exposes_first_level(model: StepModel, face_id: int) -> Non
raise SystemExit(f"Selected Face should expose 4 first-level adjacent Faces, got {feature_info}")
editable_specs, _used = probe._editable_property_specs(feature_info)
feature_rows = probe._feature_property_specs(editable_specs, feature_info)
topology_row = _spec(feature_rows, "face_first_level_topology")
topology_row = _spec(editable_specs, "face_first_level_topology")
topology_text = str(topology_row.get("current_text") or "")
for fragment in ("Face 区域 1 个", "边界 Edge 4 条", "共享边相邻 Face 4 个"):
if fragment not in topology_text:
raise SystemExit(f"Selected Face topology row is not clear enough: {topology_row}")
feature_rows = probe._feature_property_specs(editable_specs, feature_info)
feature_row_keys = {str(spec.get("key", "")) for spec in feature_rows}
if "face_first_level_topology" in feature_row_keys:
raise SystemExit(
"Feature parameter table should keep first-level topology in diagnostics, "
f"not in editable feature rows: {feature_rows}"
)
def main() -> int:
+5 -2
View File
@@ -120,8 +120,11 @@ def main() -> int:
feature_keys = {str(spec.get("key")) for spec in feature_rows}
if "no_editable_feature_dimensions" not in feature_keys:
raise SystemExit(f"feature mode should say there are no reliable dimensions: {feature_rows}")
if "freeform_surface_edit_semantics" not in feature_keys:
raise SystemExit(f"feature mode should keep the freeform limitation explanation: {feature_rows}")
if "freeform_surface_edit_semantics" in feature_keys:
raise SystemExit(
"feature parameter table should keep freeform limitations in diagnostics, "
f"not mixed into editable parameters: {feature_rows}"
)
_assert_no_editable_actions(feature_rows, "feature mode rows")
print(
+29 -1
View File
@@ -166,6 +166,19 @@ def _assert_local_scope_explains_curved_owner(specs: list[dict[str, object]], ke
raise SystemExit(f"{key}/local disabled tip should mention curved owner: {disabled_tip}")
def _assert_keep_relations_blocked(plan: dict[str, object], label: str, *expected_fragments: str) -> None:
if plan.get("status") != "blocked":
raise SystemExit(f"{label} should be blocked before execution: {plan}")
if plan.get("face_push_pull_planar_relation_constraint_requested") is not True:
raise SystemExit(f"{label} should record the requested keep-relation constraint: {plan}")
if plan.get("face_push_pull_planar_constraint_status") != "blocked":
raise SystemExit(f"{label} should expose blocked keep-relation constraint status: {plan}")
message = str(plan.get("message") or "") + " " + str(plan.get("blockers") or "")
for fragment in expected_fragments:
if fragment not in message:
raise SystemExit(f"{label} blocker should mention {fragment!r}: {plan}")
def _bbox_height(model: StepModel) -> float:
info = model.part_info(1)
bbox_size = info.get("bbox_size")
@@ -199,6 +212,12 @@ def main() -> int:
_assert_blocked(model.face_center_local_move_plan(face_id, target_center), "Face center local move")
_assert_blocked(model.face_area_local_resize_plan(face_id, area * 1.1), "Face area local resize")
_assert_blocked(model.face_plane_offset_local_plan(face_id, 1.0), "Face plane offset local move")
_assert_keep_relations_blocked(
model.push_pull_keep_relations_plan(face_id, 1.0),
"planar cylinder cap keep-relations push/pull",
"非平面",
"平面邻域",
)
specs = _specs(face_id, info)
semantics = _spec(specs, "face_edit_semantics")
@@ -206,11 +225,20 @@ def main() -> int:
raise SystemExit(f"Face edit semantics should summarize the local blocker: {semantics}")
if "曲面" not in str(semantics.get("disabled_tip") or ""):
raise SystemExit(f"Face edit semantics tip should include the curved-owner blocker: {semantics}")
for key in ("area", "face_center_position", "face_target_normal_position"):
for key in ("face_center_position", "face_target_normal_position"):
_assert_local_scope_explains_curved_owner(specs, key)
push_pull_mode = _scope_mode(specs, "face_target_normal_position", "push_pull")
if not bool(push_pull_mode.get("enabled", False)):
raise SystemExit("planar cylinder cap push/pull scope should remain available")
keep_relations_mode = _scope_mode(specs, "face_target_normal_position", "keep_relations")
if bool(keep_relations_mode.get("enabled", True)):
raise SystemExit(f"planar cylinder cap keep-relations scope should be disabled: {keep_relations_mode}")
keep_relations_tip = str(keep_relations_mode.get("disabled_tip") or "")
if "非平面" not in keep_relations_tip and "曲面" not in keep_relations_tip:
raise SystemExit(
"planar cylinder cap keep-relations disabled tip should mention non-planar adjacency: "
f"{keep_relations_tip}"
)
huge_push_plan = model.push_pull_plan(face_id, 50.0)
if huge_push_plan.get("status") == "blocked":
@@ -32,6 +32,22 @@ def _write_triangular_prism(path: Path) -> None:
_write_step(prism, path)
def _write_trapezoid_prism(path: Path) -> None:
polygon = BRepBuilderAPI_MakePolygon()
polygon.Add(gp_Pnt(0.0, 0.0, 0.0))
polygon.Add(gp_Pnt(12.0, 0.0, 0.0))
polygon.Add(gp_Pnt(10.0, 0.0, 6.0))
polygon.Add(gp_Pnt(0.0, 0.0, 8.0))
polygon.Close()
if hasattr(polygon, "IsDone") and not polygon.IsDone():
raise RuntimeError("Could not create trapezoid prism profile.")
face_maker = BRepBuilderAPI_MakeFace(polygon.Wire())
if hasattr(face_maker, "IsDone") and not face_maker.IsDone():
raise RuntimeError("Could not create trapezoid prism face.")
prism = BRepPrimAPI_MakePrism(face_maker.Face(), gp_Vec(0.0, 6.0, 0.0)).Shape()
_write_step(prism, path)
def _triangle_face_id(model: StepModel) -> int:
for face_id in range(len(model.faces)):
info = model.face_info(face_id)
@@ -42,6 +58,19 @@ def _triangle_face_id(model: StepModel) -> int:
raise SystemExit("no triangular planar Face was found")
def _sloped_quad_face_id(model: StepModel) -> int:
for face_id in range(len(model.faces)):
info = model.face_info(face_id)
if info.get("surface") != "plane":
continue
normal = info.get("normal") or info.get("push_pull_outward_direction")
if not isinstance(normal, tuple) or len(normal) != 3:
continue
if abs(float(normal[0])) > 0.1 and abs(float(normal[2])) > 0.1:
return face_id
raise SystemExit("no sloped quadrilateral planar Face was found")
def _triangle_infos(model: StepModel) -> list[dict[str, object]]:
infos: list[dict[str, object]] = []
for face_id in range(len(model.faces)):
@@ -89,6 +118,20 @@ def _assert_not_blocked(plan: dict[str, object], label: str) -> None:
raise SystemExit(f"{label} should be available for a simple triangular Face: {plan}")
def _assert_keep_relations_blocked_for_angled(plan: dict[str, object], label: str) -> None:
if plan.get("status") != "blocked":
raise SystemExit(f"{label} should be blocked before execution: {plan}")
if plan.get("face_push_pull_planar_relation_constraint_requested") is not True:
raise SystemExit(f"{label} should record the requested keep-relation constraint: {plan}")
if plan.get("face_push_pull_planar_constraint_status") != "blocked":
raise SystemExit(f"{label} should expose blocked keep-relation constraint status: {plan}")
if int(plan.get("face_push_pull_planar_constraint_angled_count", 0) or 0) <= 0:
raise SystemExit(f"{label} should count at least one angled relation: {plan}")
message = str(plan.get("message") or "") + " " + str(plan.get("blockers") or "")
if "斜交" not in message:
raise SystemExit(f"{label} blocker should explain angled first-level planar relations: {plan}")
def _assert_single_solid(model: StepModel, label: str) -> None:
stats = model.stats()
if stats.solids != 1:
@@ -162,6 +205,15 @@ def main() -> int:
model.push_pull_face(face_id, 1.0)
_assert_single_solid(model, "triangular Face push/pull")
wedge_path = Path(temp_dir) / "trapezoid_prism.step"
_write_trapezoid_prism(wedge_path)
wedge = StepModel.load(wedge_path)
wedge_face_id = _sloped_quad_face_id(wedge)
plan = wedge.push_pull_plan(wedge_face_id, 0.5)
_assert_not_blocked(plan, "sloped planar Face push/pull")
keep_plan = wedge.push_pull_keep_relations_plan(wedge_face_id, 0.5)
_assert_keep_relations_blocked_for_angled(keep_plan, "sloped planar Face keep-relations push/pull")
model, face_id, info = _fresh_model(path)
current_area = float(info.get("area") or 0.0)
target_area = current_area * 1.44
@@ -136,7 +136,11 @@ def main() -> int:
face_id = _large_stepped_cap_face(model)
logical_id = model.face_region_logical_id(face_id)
before_topology = _face_topology_counts(model, face_id)
started = time.perf_counter()
plan = model.push_pull_plan(face_id, 89.0)
plan_elapsed = time.perf_counter() - started
if plan_elapsed > 5.0:
raise SystemExit(f"large stepped cap push/pull plan took too long: {plan_elapsed:.3f}s; plan={plan}")
if plan.get("cylindrical_cap_extension_kind") != "coaxial-stepped-cap":
raise SystemExit(f"large stepped cap should be recognized as stepped cap: {plan}")
if plan.get("cylindrical_cap_extension_method") != "local-shell-rebuild":
@@ -432,7 +436,8 @@ def main() -> int:
print(
"large stepped cap push/pull ok: "
f"face_id={face_id}, elapsed={elapsed:.3f}s, isolated_elapsed={isolated_elapsed:.3f}s, "
f"face_id={face_id}, plan_elapsed={plan_elapsed:.3f}s, "
f"elapsed={elapsed:.3f}s, isolated_elapsed={isolated_elapsed:.3f}s, "
f"topology_before={before_topology}, topology_after={after_topology}, result={result}"
)
print(