from __future__ import annotations from pathlib import Path import sys import tempfile PROJECT_ROOT = Path(__file__).resolve().parent.parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) SCRIPTS_ROOT = PROJECT_ROOT / "scripts" if str(SCRIPTS_ROOT) not in sys.path: sys.path.insert(0, str(SCRIPTS_ROOT)) from step_editor.model import StepModel from step_editor.window_state import WindowStateMixin from verify_edge_length_resize import _line_edge_ids_near_length from verify_hole_resize import _write_through_hole_model DEFAULT_CUBE = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step" class _FakeCombo: def __init__(self, value: str, text: str) -> None: self._value = value self._text = text def currentData(self) -> str: return self._value def currentText(self) -> str: return self._text class _EdgeIntentProbe(WindowStateMixin): def __init__(self, *, strategy: str = "auto", anchor: str = "auto") -> None: self.model = object() self.operation_in_progress = False self.scan_in_progress = False self.load_in_progress = False self.selected_kind = "edge" self.selected_face_id = None self.selected_edge_id = 0 self.selected_part_id = 0 self.selected_solid_id = 0 self.edge_length_strategy_combo = _FakeCombo(strategy, _strategy_label(strategy)) self.edge_length_anchor_combo = _FakeCombo(anchor, _anchor_label(anchor)) def _assert(condition: bool, message: str) -> None: if not condition: raise AssertionError(message) def _strategy_label(strategy: str) -> str: return { "auto": "自动选择", "local-edge-only-deform": "只改当前Edge", "move-edge-end-plane-by-push-pull": "移动端面/保持垂直", "resize-adjacent-cylinder-from-circular-edge-length": "相邻圆柱直径", "scale-owning-shape-from-edge": "缩放所属", }.get(strategy, strategy) def _anchor_label(anchor: str) -> str: return { "auto": "自动", "center": "中心", "keep-start": "固定起点", "keep-end": "固定终点", }.get(anchor, anchor) def _spec(specs: list[dict[str, object]], key: str) -> dict[str, object]: for spec in specs: if spec.get("key") == key: return spec raise AssertionError(f"{key} spec was not found") def _scope(spec: dict[str, object], key: str) -> dict[str, object]: modes = spec.get("scope_modes") if not isinstance(modes, dict): raise AssertionError(f"{spec.get('key')} should expose modeling-intent scope modes") mode = modes.get(key) if not isinstance(mode, dict): raise AssertionError(f"{spec.get('key')} has no scope mode {key}") return mode def _specs_for_edge(info: dict[str, object], *, strategy: str = "auto", anchor: str = "auto") -> list[dict[str, object]]: probe = _EdgeIntentProbe(strategy=strategy, anchor=anchor) specs, _used = probe._editable_property_specs(info) return specs def _preselect_value(mode: dict[str, object]) -> str: preselects = mode.get("preselect_combos") if not isinstance(preselects, (tuple, list)) or not preselects: raise AssertionError(f"scope mode should synchronize the hidden Edge strategy combo: {mode}") first = preselects[0] if not isinstance(first, dict): raise AssertionError(f"invalid preselect combo spec: {mode}") return str(first.get("value") or "") def _assert_edge_length_scope(spec: dict[str, object], mode_key: str, label: str, *, enabled: bool) -> None: mode = _scope(spec, mode_key) _assert(str(mode.get("label")) == label, f"{mode_key} label should be {label!r}, got {mode.get('label')!r}") _assert(str(mode.get("action")) == "resize_any_edge_length", f"{mode_key} should call resize_any_edge_length") _assert(str(mode.get("target_attr")) == "edge_target_length_input", f"{mode_key} should target Edge length input") _assert(bool(mode.get("enabled")) is enabled, f"{mode_key} enabled should be {enabled}, got {mode.get('enabled')}") _assert(_preselect_value(mode) == mode_key, f"{mode_key} should preselect itself before execution") hint = f"{mode.get('enabled_tip', '')} {mode.get('range_hint', '')}" _assert(label in hint or mode_key == "auto", f"{mode_key} should explain its modeling intent") def _verify_line_edge_intent_specs() -> None: model = StepModel.load(DEFAULT_CUBE) edge_ids = _line_edge_ids_near_length(model, 10.0, 1e-5) if not edge_ids: raise AssertionError("cube has no 10mm line Edge") info = model.edge_info(edge_ids[0]) specs = _specs_for_edge(info, strategy="move-edge-end-plane-by-push-pull", anchor="keep-start") _assert(str(_spec(specs, "edge_edit_semantics").get("label")) == "建模意图", "Edge semantic row should be labeled 建模意图") length_spec = _spec(specs, "length") _assert(str(length_spec.get("label")) == "长度", f"Edge length label should stay user-facing: {length_spec}") _assert( str(length_spec.get("scope_default")) == "move-edge-end-plane-by-push-pull", f"Edge length should honor selected modeling intent: {length_spec.get('scope_default')}", ) _assert_edge_length_scope(length_spec, "auto", "自动", enabled=True) _assert_edge_length_scope(length_spec, "local-edge-only-deform", "只改当前Edge", enabled=True) _assert_edge_length_scope(length_spec, "move-edge-end-plane-by-push-pull", "移动端面", enabled=True) _assert_edge_length_scope( length_spec, "resize-adjacent-cylinder-from-circular-edge-length", "相邻圆柱", enabled=False, ) _assert_edge_length_scope(length_spec, "scale-owning-shape-from-edge", "缩放所属", enabled=True) anchor_spec = _spec(specs, "edge_length_anchor_mode") _assert(str(anchor_spec.get("label")) == "长度基准", "line Edge should expose a length anchor row") print("line Edge modeling-intent specs ok") def _circle_edge(model: StepModel) -> int: for edge_id in range(len(model.edges)): info = model.edge_info(edge_id) if info.get("curve") == "circle" and info.get("radius") and info.get("length"): return edge_id raise AssertionError("model has no circular Edge") def _verify_circle_edge_intent_specs() -> None: with tempfile.TemporaryDirectory(prefix="geom_param_edge_intent_circle_") as temp_dir: model_path = Path(temp_dir) / "hole.step" _write_through_hole_model(model_path) model = StepModel.load(model_path) info = model.edge_info(_circle_edge(model)) specs = _specs_for_edge( info, strategy="resize-adjacent-cylinder-from-circular-edge-length", anchor="auto", ) length_spec = _spec(specs, "length") _assert( str(length_spec.get("scope_default")) == "resize-adjacent-cylinder-from-circular-edge-length", "circular Edge length should honor adjacent-cylinder intent", ) _assert_edge_length_scope(length_spec, "local-edge-only-deform", "只改当前Edge", enabled=False) _assert_edge_length_scope(length_spec, "move-edge-end-plane-by-push-pull", "移动端面", enabled=False) _assert_edge_length_scope( length_spec, "resize-adjacent-cylinder-from-circular-edge-length", "相邻圆柱", enabled=True, ) radius_spec = _spec(specs, "circle_edge_radius") radius_modes = radius_spec.get("scope_modes") _assert( isinstance(radius_modes, dict) and set(radius_modes) == {"auto", "resize-adjacent-cylinder-from-circular-edge-length", "scale-owning-shape-from-edge"}, f"circle Edge radius should expose only circular modeling intents: {radius_modes}", ) for mode_key in radius_modes: _assert(_preselect_value(radius_modes[mode_key]) == mode_key, f"{mode_key} radius scope should sync strategy") print("circular Edge modeling-intent specs ok") def _verify_edge_length_plan_intents() -> None: model = StepModel.load(DEFAULT_CUBE) edge_id = _line_edge_ids_near_length(model, 10.0, 1e-5)[0] expectations = ( ("local-edge-only-deform", "keep-start", "local-edge-only-deform", "相邻面会自然变斜"), ("move-edge-end-plane-by-push-pull", "keep-start", "move-edge-end-plane-by-push-pull", "正方体这类模型会更像变成长方体"), ("scale-owning-shape-from-edge", "center", "scale-owning-shape-from-edge", "其它尺寸会跟随变化"), ) for strategy, anchor, expected_strategy, impact_fragment in expectations: plan = model.general_edge_length_plan(edge_id, 15.0, anchor_mode=anchor, strategy_mode=strategy) _assert(str(plan.get("status")) != "blocked", f"{strategy}/{anchor} should be available: {plan}") _assert( str(plan.get("resize_strategy")) == expected_strategy, f"{strategy}/{anchor} resolved to {plan.get('resize_strategy')}, expected {expected_strategy}", ) impact = str(plan.get("edge_length_impact_summary") or "") _assert(impact_fragment in impact, f"{strategy} impact summary should explain result: {impact}") blocked = model.general_edge_length_plan( edge_id, 15.0, anchor_mode="center", strategy_mode="move-edge-end-plane-by-push-pull", ) _assert(str(blocked.get("status")) == "blocked", "moving an end plane while fixing center should be blocked") _assert("不能使用固定中心" in str(blocked.get("message") or ""), f"blocked message should explain center conflict: {blocked}") print("Edge modeling-intent plans ok") def main() -> int: _verify_line_edge_intent_specs() _verify_circle_edge_intent_specs() _verify_edge_length_plan_intents() print("Edge modeling-intent spec suite passed.") return 0 if __name__ == "__main__": raise SystemExit(main())