230 lines
9.7 KiB
Python
230 lines
9.7 KiB
Python
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())
|