Files

320 lines
14 KiB
Python
Raw Permalink Normal View History

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": "移动端面/保持垂直",
"keep-first-level-planar-relations": "保持一级平面关系",
"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, "keep-first-level-planar-relations", "保持关系", 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)
constrained_specs = _specs_for_edge(info, strategy="keep-first-level-planar-relations", anchor="keep-start")
constrained_length_spec = _spec(constrained_specs, "length")
_assert(
str(constrained_length_spec.get("scope_default")) == "keep-first-level-planar-relations",
"line Edge should honor the explicit keep-planar-relations modeling intent",
)
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, "keep-first-level-planar-relations", "保持关系", 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", "正方体这类模型会更像变成长方体"),
("keep-first-level-planar-relations", "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}")
if expected_strategy == "move-edge-end-plane-by-push-pull":
_assert(
plan.get("edge_length_planar_constraint_status") == "ready",
f"end-face push/pull should expose ready planar constraints: {plan}",
)
_assert(
"端面推拉平面约束" in str(plan.get("edge_length_planar_constraint_summary") or ""),
f"end-face push/pull should explain its planar constraint guard: {plan}",
)
_assert(
int(plan.get("edge_length_planar_constraint_perpendicular_count", 0) or 0) >= 1,
f"end-face push/pull should detect perpendicular side-face relations: {plan}",
)
if strategy == "keep-first-level-planar-relations":
_assert(
plan.get("edge_length_planar_relation_constraint_requested") is True,
f"explicit keep-planar-relations strategy should be preserved in the plan: {plan}",
)
_assert(
str(plan.get("edge_length_strategy_label")) == "保持一级平面关系",
f"explicit keep-planar-relations strategy should keep a user-facing label: {plan}",
)
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}")
blocked_keep_relations = model.general_edge_length_plan(
edge_id,
15.0,
anchor_mode="center",
strategy_mode="keep-first-level-planar-relations",
)
_assert(
str(blocked_keep_relations.get("status")) == "blocked",
"keeping first-level planar relations while fixing center should be blocked",
)
_assert(
"保持一级平面关系" in str(blocked_keep_relations.get("message") or ""),
f"blocked keep-planar-relations plan should name the requested constraint: {blocked_keep_relations}",
)
print("Edge modeling-intent plans ok")
def _verify_edge_coordinate_move_plan_semantics() -> None:
model = StepModel.load(DEFAULT_CUBE)
edge_id = _line_edge_ids_near_length(model, 10.0, 1e-5)[0]
info = model.edge_info(edge_id)
start = info.get("start_point")
center = info.get("length_center")
if not isinstance(start, (tuple, list)) or len(start) != 3:
raise AssertionError(f"line Edge lacks a stable start point: {info}")
if not isinstance(center, (tuple, list)) or len(center) != 3:
raise AssertionError(f"line Edge lacks a stable center: {info}")
endpoint_plan = model.edge_endpoint_move_plan(
edge_id,
"start",
(float(start[0]) - 1.0, float(start[1]), float(start[2])),
)
_assert(str(endpoint_plan.get("status")) != "blocked", f"endpoint move should be available: {endpoint_plan}")
_assert(str(endpoint_plan.get("edge_endpoint_label")) == "起点", f"endpoint label should be Chinese: {endpoint_plan}")
_assert(
str(endpoint_plan.get("edit_strategy_label")) == "Edge端点局部移动",
f"endpoint move should expose a user-facing strategy label: {endpoint_plan}",
)
_assert(
"不是端面整体推拉" in str(endpoint_plan.get("edit_semantics") or ""),
f"endpoint move should explain that it is local deformation: {endpoint_plan}",
)
center_plan = model.edge_center_move_plan(
edge_id,
(float(center[0]), float(center[1]), float(center[2]) + 1.0),
)
_assert(str(center_plan.get("status")) != "blocked", f"center move should be available: {center_plan}")
_assert(
str(center_plan.get("edit_strategy_label")) == "Edge整体局部移动",
f"center move should expose a user-facing strategy label: {center_plan}",
)
_assert(
"不是移动整个端面" in str(center_plan.get("edit_semantics") or ""),
f"center move should explain that it is local deformation: {center_plan}",
)
print("Edge coordinate move plan semantics ok")
def main() -> int:
_verify_line_edge_intent_specs()
_verify_circle_edge_intent_specs()
_verify_edge_length_plan_intents()
_verify_edge_coordinate_move_plan_semantics()
print("Edge modeling-intent spec suite passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())