feat: 完善一级关系参数化建模与参数导出
This commit is contained in:
@@ -17,10 +17,10 @@ FACE_ID = 594
|
||||
BASE_FACE_KEYS = (
|
||||
"local_face_width",
|
||||
"local_face_height",
|
||||
"face_center_position",
|
||||
"face_target_normal_position",
|
||||
)
|
||||
RESULT_ONLY_KEYS = ("area",)
|
||||
TEMPORARILY_HIDDEN_KEYS = ("face_center_position",)
|
||||
|
||||
|
||||
class _Probe(WindowStateMixin):
|
||||
@@ -130,6 +130,7 @@ 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")
|
||||
_assert_absent(before_cached_rows, TEMPORARILY_HIDDEN_KEYS, "Face 594 before full feature cache")
|
||||
|
||||
full_info = model.feature_info(FACE_ID)
|
||||
if full_info.get("shell_region_status") == "candidate":
|
||||
@@ -142,6 +143,7 @@ def main() -> int:
|
||||
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")
|
||||
_assert_absent(after_cached_rows, TEMPORARILY_HIDDEN_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: "
|
||||
|
||||
@@ -133,8 +133,6 @@ PROPERTY_FACE_ACTION_TO_ISOLATED_OPERATION = {
|
||||
"resize_face_height_keep_relations": "resize_face_size_local_keep_relations",
|
||||
"resize_face_width_owning_scale": "resize_face_size_owning_scale",
|
||||
"resize_face_height_owning_scale": "resize_face_size_owning_scale",
|
||||
"move_selected_face_center_local": "move_face_center_local",
|
||||
"move_selected_face_center_keep_relations": "move_face_center_local_keep_relations",
|
||||
"resize_shell_thickness": "resize_shell_thickness",
|
||||
"resize_shell_thickness_owning_scale": "resize_shell_thickness_owning_scale",
|
||||
"resize_cylinder_height": "resize_cylindrical_height",
|
||||
@@ -283,7 +281,6 @@ def _face_property_actions_from_specs() -> set[str]:
|
||||
"area",
|
||||
"local_face_width",
|
||||
"local_face_height",
|
||||
"face_center_position",
|
||||
"face_target_normal_position",
|
||||
},
|
||||
),
|
||||
|
||||
@@ -51,12 +51,11 @@ def main() -> int:
|
||||
"feature_guess": "round/fillet candidate",
|
||||
"angular_span": math.pi / 2.0,
|
||||
},
|
||||
("existing_fillet_radius_estimate",),
|
||||
("existing_fillet_radius_estimate", "existing_fillet_arc_length_estimate"),
|
||||
)
|
||||
assert_keys(
|
||||
{"surface": "plane"},
|
||||
(
|
||||
"area",
|
||||
"local_face_width",
|
||||
"local_face_height",
|
||||
"face_center_position",
|
||||
@@ -66,7 +65,6 @@ def main() -> int:
|
||||
assert_keys(
|
||||
{"surface": "plane", "shell_region_status": "candidate"},
|
||||
(
|
||||
"area",
|
||||
"local_face_width",
|
||||
"local_face_height",
|
||||
"face_center_position",
|
||||
@@ -123,7 +121,7 @@ def main() -> int:
|
||||
},
|
||||
)
|
||||
filtered_keys = tuple(str(spec.get("key")) for spec in filtered)
|
||||
if filtered_keys != ("diameter", "hole_edit_semantics"):
|
||||
if filtered_keys != ("diameter",):
|
||||
raise AssertionError(f"unexpected filtered feature parameters: {filtered_keys}")
|
||||
|
||||
print("feature parameter policy ok")
|
||||
|
||||
@@ -272,6 +272,34 @@ def _verify_user_priority_scan_order(root: Path) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _verify_candidate_scan_cache(root: Path) -> None:
|
||||
path = root / "candidate_cache.step"
|
||||
_write_through_hole_model(path)
|
||||
model = StepModel.load(path)
|
||||
|
||||
first = model.editable_feature_candidates(limit=16, detailed=False, max_scan_faces=120, max_scan_edges=120)
|
||||
_assert(first, "editable candidate cache probe returned no candidates")
|
||||
cache = getattr(model, "_editable_feature_candidates_cache", {})
|
||||
_assert(cache, "editable candidate scan should populate the model-level cache")
|
||||
first[0]["operation_key"] = "mutated-by-caller"
|
||||
second = model.editable_feature_candidates(limit=16, detailed=False, max_scan_faces=120, max_scan_edges=120)
|
||||
_assert(
|
||||
second[0].get("operation_key") != "mutated-by-caller",
|
||||
"editable candidate cache should return defensive copies",
|
||||
)
|
||||
|
||||
cylinders = model.cylindrical_feature_candidates(limit=12, include_end_info=True, max_scan_faces=120)
|
||||
_assert(cylinders, "cylindrical candidate cache probe returned no candidates")
|
||||
cylinder_cache = getattr(model, "_cylindrical_feature_candidates_cache", {})
|
||||
_assert(cylinder_cache, "cylindrical candidate scan should populate the model-level cache")
|
||||
cylinders[0]["feature_guess"] = "mutated-by-caller"
|
||||
second_cylinders = model.cylindrical_feature_candidates(limit=12, include_end_info=True, max_scan_faces=120)
|
||||
_assert(
|
||||
second_cylinders[0].get("feature_guess") != "mutated-by-caller",
|
||||
"cylindrical candidate cache should return defensive copies",
|
||||
)
|
||||
|
||||
|
||||
def _verify_ellipse_edge_scan_entries(root: Path) -> None:
|
||||
path = root / "ellipse_edge_scan.step"
|
||||
_write_ellipse_face_model(path)
|
||||
@@ -405,6 +433,7 @@ def main() -> int:
|
||||
_verify_boss_summary(root)
|
||||
_verify_torus_summary(root)
|
||||
_verify_user_priority_scan_order(root)
|
||||
_verify_candidate_scan_cache(root)
|
||||
_verify_ellipse_edge_scan_entries(root)
|
||||
_verify_complex_slot_guard(root)
|
||||
_verify_mixed_radius_fillet_chain_guard(root)
|
||||
|
||||
@@ -59,7 +59,7 @@ def _verify_readme_mentions(readme: str) -> None:
|
||||
"[不能修改 -> 立即说明原因]",
|
||||
"[一级影响范围 -> 明确显示]",
|
||||
"Face 阶段的当前验收口径(R1 已收口)",
|
||||
"已验收:平面 Face 的 `面内长度`、`面内宽度`、`中心`、`偏移`",
|
||||
"已验收:平面 Face 的 `面内长度`、`面内宽度`、`偏移`",
|
||||
"未实现/不承诺:原 CAD 历史恢复、任意复杂 Face 的通用局部重建",
|
||||
"孔/槽阶段的当前验收口径(R2/R3 已收口)",
|
||||
"已验收:圆柱孔/盲孔的 `直径`、`半径`、`轴心`、`盲孔深度`",
|
||||
|
||||
@@ -417,7 +417,6 @@ def main() -> int:
|
||||
("local_face_width", "长度", "10"),
|
||||
("local_face_height", "宽度", "8"),
|
||||
("shell_thickness_estimate", "高度/深度", "3"),
|
||||
("face_center_position", "中心", "(15, 10, 2)"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -438,7 +437,6 @@ def main() -> int:
|
||||
("local_face_width", "长度", "10"),
|
||||
("local_face_height", "宽度", "8"),
|
||||
("shell_thickness_estimate", "高度/深度", "3"),
|
||||
("face_center_position", "中心", "(15, 10, 8)"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -499,7 +497,6 @@ def main() -> int:
|
||||
("local_face_width", "长度", "6"),
|
||||
("local_face_height", "宽度", "4"),
|
||||
("shell_thickness_estimate", "高度/深度", "2"),
|
||||
("face_center_position", "中心", "(15, 10, 10)"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PySide6.QtCore import QEvent, QObject
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
@@ -30,8 +33,10 @@ from step_editor.window_actions import WindowActionMixin
|
||||
from step_editor.window_core import WindowCoreMixin
|
||||
from step_editor.window_state import (
|
||||
PROPERTY_CURRENT_COLUMN,
|
||||
PROPERTY_INPUT_COLUMN,
|
||||
PROPERTY_LABEL_COLUMN,
|
||||
PROPERTY_SCOPE_COLUMN,
|
||||
PROPERTY_TABLE_HEADERS,
|
||||
PROPERTY_TARGET_COLUMN,
|
||||
WindowStateMixin,
|
||||
)
|
||||
@@ -66,8 +71,8 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.object_edit_box = self
|
||||
self.property_table = QTableWidget(0, 4)
|
||||
self.property_table.setHorizontalHeaderLabels(["尺寸参数", "当前值", "建模意图", "目标值"])
|
||||
self.property_table = QTableWidget(0, len(PROPERTY_TABLE_HEADERS))
|
||||
self.property_table.setHorizontalHeaderLabels(list(PROPERTY_TABLE_HEADERS))
|
||||
layout.addWidget(self.property_table)
|
||||
|
||||
self.property_card_scroll = QScrollArea()
|
||||
@@ -85,6 +90,7 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
||||
self.property_command_help_label = QLabel()
|
||||
self.current_capability_headline = QLabel()
|
||||
self.apply_property_button = QPushButton()
|
||||
self.export_parameters_button = QPushButton()
|
||||
|
||||
@staticmethod
|
||||
def _plane_info() -> dict[str, object]:
|
||||
@@ -121,6 +127,44 @@ class _ActionMessageProbe(WindowActionMixin):
|
||||
pass
|
||||
|
||||
|
||||
class _ParameterExportActionProbe(WindowActionMixin):
|
||||
def __init__(self, output_path: Path) -> None:
|
||||
self.output_path = output_path
|
||||
self.status_bar = _StatusBarProbe()
|
||||
self.info_text = ""
|
||||
self.export_state_updates = 0
|
||||
|
||||
def _parameter_export_output_path(self) -> Path:
|
||||
return self.output_path
|
||||
|
||||
def _selected_parameter_export_rows(self) -> list[dict[str, str]]:
|
||||
return [
|
||||
{
|
||||
"name": "面内长度",
|
||||
"displayName": "面内长度",
|
||||
"type": "number",
|
||||
"ioRole": "input",
|
||||
"default": "151",
|
||||
},
|
||||
{
|
||||
"name": "偏移",
|
||||
"displayName": "偏移",
|
||||
"type": "number",
|
||||
"ioRole": "input",
|
||||
"default": "57.5",
|
||||
},
|
||||
]
|
||||
|
||||
def _update_parameter_export_state(self) -> None:
|
||||
self.export_state_updates += 1
|
||||
|
||||
def statusBar(self) -> _StatusBarProbe:
|
||||
return self.status_bar
|
||||
|
||||
def set_plain_info(self, text: str) -> None:
|
||||
self.info_text = text
|
||||
|
||||
|
||||
class _TimerProbe:
|
||||
def stop(self) -> None:
|
||||
pass
|
||||
@@ -240,7 +284,7 @@ def _assert_property_table_editor(probe: _PropertyTableProbe) -> None:
|
||||
probe.property_table.horizontalHeaderItem(column).text()
|
||||
for column in range(probe.property_table.columnCount())
|
||||
]
|
||||
_assert(headers == ["尺寸参数", "当前值", "建模意图", "目标值"], f"unexpected table headers: {headers}")
|
||||
_assert(headers == list(PROPERTY_TABLE_HEADERS), f"unexpected table headers: {headers}")
|
||||
_assert(probe.property_table.rowCount() == len(probe.property_editor_specs), "table row count should match specs")
|
||||
header_height = int(probe.property_table.horizontalHeader().height())
|
||||
frame = int(probe.property_table.frameWidth()) * 2
|
||||
@@ -262,8 +306,10 @@ def _assert_property_table_editor(probe: _PropertyTableProbe) -> None:
|
||||
if probe.property_table.item(row, PROPERTY_LABEL_COLUMN) is not None
|
||||
]
|
||||
actionable_labels = [str(spec.get("label", "")) for _row, spec in probe._actionable_property_rows()]
|
||||
for label in ("面内长度", "面内宽度", "中心", "偏移"):
|
||||
for label in ("面内长度", "面内宽度", "偏移"):
|
||||
_assert(label in actionable_labels, f"feature parameter table did not expose {label}")
|
||||
_assert("中心" not in actionable_labels, "center should be temporarily hidden from editable parameters")
|
||||
_assert("中心" not in table_labels, "center should not appear in the feature parameter table")
|
||||
for legacy_label in ("面积", "U向尺寸", "V向尺寸", "偏移变换"):
|
||||
_assert(legacy_label not in actionable_labels, f"{legacy_label} should not be exposed as an editable parameter")
|
||||
_assert(legacy_label not in table_labels, f"{legacy_label} should not appear in the feature parameter table")
|
||||
@@ -273,13 +319,38 @@ def _assert_property_table_editor(probe: _PropertyTableProbe) -> None:
|
||||
target_row = _row_by_label(probe, "面内长度")
|
||||
target_widget = probe.property_table.cellWidget(target_row, PROPERTY_TARGET_COLUMN)
|
||||
scope_widget = probe.property_table.cellWidget(target_row, PROPERTY_SCOPE_COLUMN)
|
||||
input_checkbox = probe._property_input_checkbox(target_row)
|
||||
_assert(isinstance(target_widget, QLineEdit), "editable table row should have a target editor")
|
||||
_assert(isinstance(scope_widget, NoWheelComboBox), "editable table row should have a modeling-intent combo")
|
||||
_assert(isinstance(input_checkbox, QCheckBox), "editable table row should have an input-parameter checkbox")
|
||||
_assert(not input_checkbox.isChecked(), "input-parameter checkbox should be unchecked by default")
|
||||
_assert(
|
||||
probe.property_table.cellWidget(target_row, PROPERTY_INPUT_COLUMN) is not None,
|
||||
"input-parameter checkbox should be hosted in the input column",
|
||||
)
|
||||
_assert(not probe.export_parameters_button.isEnabled(), "parameter export button should start disabled")
|
||||
_assert(
|
||||
not probe.property_table.findChildren(QPushButton),
|
||||
"feature parameter table should not contain per-row apply buttons",
|
||||
)
|
||||
|
||||
input_checkbox.setChecked(True)
|
||||
QApplication.processEvents()
|
||||
selected_rows = probe._selected_parameter_export_rows()
|
||||
_assert(probe.export_parameters_button.isEnabled(), "parameter export button should enable after a row is checked")
|
||||
_assert(
|
||||
selected_rows == [
|
||||
{
|
||||
"name": "面内长度",
|
||||
"displayName": "面内长度",
|
||||
"type": "number",
|
||||
"ioRole": "input",
|
||||
"default": "10",
|
||||
}
|
||||
],
|
||||
f"unexpected parameter export payload: {selected_rows}",
|
||||
)
|
||||
|
||||
target_widget.setText("12")
|
||||
probe._update_property_apply_state()
|
||||
changed = probe._changed_property_rows()
|
||||
@@ -470,6 +541,35 @@ def _assert_user_facing_failure_messages() -> None:
|
||||
_assert("隔离子进程" not in empty_message and "子进程" not in empty_message, "empty internal failure should stay user-facing")
|
||||
|
||||
|
||||
def _assert_parameter_export_action() -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
output_path = Path(temp_dir) / "data.json"
|
||||
probe = _ParameterExportActionProbe(output_path)
|
||||
probe.export_selected_parameters()
|
||||
payload = json.loads(output_path.read_text(encoding="utf-8"))
|
||||
_assert(
|
||||
payload == [
|
||||
{
|
||||
"name": "面内长度",
|
||||
"displayName": "面内长度",
|
||||
"type": "number",
|
||||
"ioRole": "input",
|
||||
"default": "151",
|
||||
},
|
||||
{
|
||||
"name": "偏移",
|
||||
"displayName": "偏移",
|
||||
"type": "number",
|
||||
"ioRole": "input",
|
||||
"default": "57.5",
|
||||
},
|
||||
],
|
||||
f"parameter export action wrote unexpected JSON: {payload}",
|
||||
)
|
||||
_assert(probe.status_bar.messages and "已导出 2 个输入参数" in probe.status_bar.messages[-1], "parameter export status should mention exported count")
|
||||
_assert("data.json" in probe.info_text and "参数数量:2" in probe.info_text, "parameter export info panel summary should be useful")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
app = QApplication.instance() or QApplication([])
|
||||
top_level_label_probe = _TopLevelPropertyLabelProbe()
|
||||
@@ -497,6 +597,7 @@ def main() -> int:
|
||||
_assert_mouse_selection_guards()
|
||||
_assert_quick_blind_depth_spec()
|
||||
_assert_user_facing_failure_messages()
|
||||
_assert_parameter_export_action()
|
||||
|
||||
print("property table editor UI ok")
|
||||
if QApplication.instance() is app:
|
||||
|
||||
@@ -335,17 +335,21 @@ def _assert_target_change_detection() -> None:
|
||||
def _assert_property_table_column_widths() -> None:
|
||||
for width in (320, 340, 360, 400, 520):
|
||||
columns = _property_table_column_widths(width)
|
||||
if len(columns) != 4:
|
||||
raise SystemExit(f"property table should have four column widths, got {columns}")
|
||||
if len(columns) != 5:
|
||||
raise SystemExit(f"property table should have five column widths, got {columns}")
|
||||
if sum(columns) != width:
|
||||
raise SystemExit(f"property table widths should fill viewport {width}, got {columns} sum={sum(columns)}")
|
||||
label_width, current_width, scope_width, target_width = columns
|
||||
if current_width < 96:
|
||||
label_width, current_width, scope_width, target_width, input_width = columns
|
||||
if label_width < 58:
|
||||
raise SystemExit(f"dimension name column should stay visible at {width}: {columns}")
|
||||
if current_width < 64:
|
||||
raise SystemExit(f"current value column should stay readable at {width}: {columns}")
|
||||
if target_width < 56:
|
||||
if target_width < 54:
|
||||
raise SystemExit(f"target value column should stay usable at {width}: {columns}")
|
||||
if scope_width < 44:
|
||||
if scope_width < 52:
|
||||
raise SystemExit(f"modeling-intent column should stay usable at {width}: {columns}")
|
||||
if input_width < 48:
|
||||
raise SystemExit(f"input-parameter checkbox column should stay usable at {width}: {columns}")
|
||||
|
||||
|
||||
def _assert_holed_plane_local_scopes_disabled() -> None:
|
||||
@@ -465,6 +469,8 @@ def main() -> int:
|
||||
plane_display_specs = _display_specs(plane_info)
|
||||
if any(str(spec.get("key", "")) == "area" for spec in plane_display_specs):
|
||||
raise SystemExit("Face property table should keep area in diagnostics, not in the parameter table")
|
||||
if any(str(spec.get("key", "")) == "face_center_position" for spec in plane_display_specs):
|
||||
raise SystemExit("Face property table should temporarily hide center editing")
|
||||
_assert_keys_absent(
|
||||
plane_display_specs,
|
||||
(
|
||||
@@ -473,6 +479,7 @@ def main() -> int:
|
||||
"face_first_level_topology",
|
||||
"face_edit_semantics",
|
||||
"feature_context_note",
|
||||
"face_center_position",
|
||||
),
|
||||
"plane Face display specs",
|
||||
)
|
||||
@@ -530,7 +537,6 @@ def main() -> int:
|
||||
(
|
||||
"local_face_width",
|
||||
"local_face_height",
|
||||
"face_center_position",
|
||||
"face_target_normal_position",
|
||||
),
|
||||
"plane Face display order",
|
||||
@@ -555,7 +561,6 @@ def main() -> int:
|
||||
expected_plane_feature_keys = {
|
||||
"local_face_width",
|
||||
"local_face_height",
|
||||
"face_center_position",
|
||||
"face_target_normal_position",
|
||||
}
|
||||
missing_plane_feature_keys = expected_plane_feature_keys - plane_feature_keys
|
||||
|
||||
Reference in New Issue
Block a user