feat: 接入 SCDM 优先编辑闭环并支持阵列局部间距
接入 SCDM probe/edit/cache/校验链路,增强孔组、阵列、关系式和参数表交互。 支持阵列相邻段间距、移动意图切换、结果回滚校验,并补充对应回归脚本。
This commit is contained in:
@@ -47,27 +47,30 @@ def _verify_readme_mentions(readme: str) -> None:
|
||||
"当前整体验证基线",
|
||||
"不等于 CAD 级完成",
|
||||
"Face 阶段的当前验收口径",
|
||||
"参数化编辑路线图",
|
||||
"用户最常用优先 > B-Rep 上稳定可实现 > 参数语义清楚",
|
||||
"`[x]` 已实现",
|
||||
"`[~]` 部分实现/进行中",
|
||||
"`[ ]` 未实现",
|
||||
"`[x]` 不是“所有 CAD 形态都能改”",
|
||||
"R1 Face 是当前主线的第一阶段收口对象",
|
||||
"R8 的二级/三级传播、跨特征约束和特征组联动不算在 0~7 完成度里",
|
||||
"STEP/B-Rep 参数化编辑主线",
|
||||
"[不能修改 -> 立即说明原因]",
|
||||
"[一级影响范围 -> 明确显示]",
|
||||
"Face 阶段的当前验收口径(R1 已收口)",
|
||||
"SCDM-first 主路线",
|
||||
"核心路线只保留下面这一棵树",
|
||||
"SCDM 内部如何处理相邻面、圆角链、二级/三级拓扑传播,交给 SCDM",
|
||||
"本软件不再把手写一级、二级、三级传播当成新增能力主线",
|
||||
"`[x]` 已适配",
|
||||
"`[~]` 部分适配",
|
||||
"`[ ]` 待适配",
|
||||
"[scdm_probe_job.json -> /RunScript 扫描 STEP]",
|
||||
"[scdm_feature_cache.json -> 映射为本软件能力字典和中文参数]",
|
||||
"[参数化建模 -> 多个目标值统一提交,不再每行一个操作按钮]",
|
||||
"[Face 偏移 -> face.offset / OffsetFaces]",
|
||||
"[槽宽 -> slot.width / OffsetFaces",
|
||||
"[槽深 -> slot.depth / Move 或 OffsetFaces",
|
||||
"[本地 OCCT -> 只保留已验证兜底能力,不再作为新主线扩展]",
|
||||
"Face 阶段的当前验收口径(本地 OCCT 兜底基线,R1 已收口)",
|
||||
"已验收:平面 Face 的 `偏移`,稳定矩形/简单全平面 Face 的 `面内长度`、`面内宽度`",
|
||||
"未实现/不承诺:原 CAD 历史恢复、任意复杂 Face 的通用局部重建",
|
||||
"孔/槽阶段的当前验收口径(R2/R3 已收口)",
|
||||
"孔/槽阶段的当前验收口径(本地 OCCT 兜底基线,R2/R3 已收口)",
|
||||
"已验收:圆柱孔/盲孔的 `直径`、`半径`、`轴心`、`盲孔深度`",
|
||||
"已验收:槽/半孔/长圆槽的 `槽宽`、`槽深`、`圆弧长度`",
|
||||
"未实现/不承诺:孔组、阵列孔、同尺寸孔联动、多槽组联动",
|
||||
"2026-08-11,在 `pyocc` 环境下已通过 `python scripts\\verify_first_level_edit_suites.py --stage hole-slot`",
|
||||
"覆盖 R2/R3 孔槽专项套件、隔离执行、逻辑 Face ID 保持和孔槽阶段收口口径",
|
||||
"0~7 其它阶段的当前基线",
|
||||
"本地 OCCT 兜底能力的当前基线",
|
||||
"verify_first_level_edit_suites.py --quick",
|
||||
"verify_first_level_edit_suites.py --stage face",
|
||||
"verify_first_level_edit_suites.py --stage hole-slot",
|
||||
@@ -92,42 +95,56 @@ def _verify_readme_mentions(readme: str) -> None:
|
||||
|
||||
|
||||
def _verify_roadmap_scope(readme: str) -> None:
|
||||
start_marker = "STEP/B-Rep 参数化编辑主线"
|
||||
end_marker = "└── 8. 二级 / 三级关系"
|
||||
start_marker = "SCDM-first 主路线"
|
||||
end_marker = "└── 7. 交付与兜底边界"
|
||||
start = readme.find(start_marker)
|
||||
end = readme.find(end_marker)
|
||||
_assert(start >= 0 and end > start, "README roadmap should contain a 0~7 active scope before stage 8")
|
||||
_assert(start >= 0 and end > start, "README should contain a single SCDM-first roadmap before fallback boundary")
|
||||
active_scope = readme[start:end]
|
||||
deferred_scope = readme[end:]
|
||||
|
||||
required_active_fragments = (
|
||||
"├── 0. 先让用户知道“能不能改”",
|
||||
"│ ├── [x] [不能修改 -> 立即说明原因]",
|
||||
"│ └── [x] [路线图 -> 验收脚本守门]",
|
||||
"├── 1. 平面 Face,第一条主线",
|
||||
"├── 2. 孔,第二条主线",
|
||||
"├── 3. 槽 / 长圆孔,从孔扩展到组合切除特征",
|
||||
"├── 4. 凸台 / Boss,从切除特征扩展到加料特征",
|
||||
"├── 5. 圆角 / 倒角,从主形体扩展到边修饰",
|
||||
"├── 6. Edge 一级编辑,补齐底层直接改边能力",
|
||||
"├── 7. 壳体 / 解析曲面,补齐高价值但边界更窄的能力",
|
||||
"├── 0. 后端发现与可用性",
|
||||
"│ ├── [x] [自动发现 SpaceClaim.exe -> 缓存路径、来源、版本和验证结果]",
|
||||
"├── 1. SCDM 识别与缓存",
|
||||
"│ ├── [x] [scdm_probe_job.json -> /RunScript 扫描 STEP]",
|
||||
"│ ├── [x] [scdm_feature_cache.json -> 映射为本软件能力字典和中文参数]",
|
||||
"├── 2. 能力字典、参数表和用户入口",
|
||||
"├── 3. SCDM 参数化建模执行",
|
||||
"├── 4. 结果校验、回滚和 ID 续接",
|
||||
"├── 5. 当前已开放或正在开放的 SCDM 能力",
|
||||
"[阵列间距 -> pattern.spacing / Move,整体阵列保持中心不变并等距重排;安全范围由支撑面动态计算,不针对 Face92 写死]",
|
||||
"[局部间距 -> pattern.segment_spacing / Move,按“FaceA-FaceB 间距”或“零件A-零件B 间距”修改相邻段;已支持固定前项移动后侧、固定后项移动前侧、两侧均分保持中心]",
|
||||
"├── 6. 下一批只按 SCDM 能力适配",
|
||||
"[阵列实例位置 -> pattern.instance_position]",
|
||||
"[壳体厚度 -> shell.thickness,薄壁候选配对已进 cache",
|
||||
)
|
||||
for fragment in required_active_fragments:
|
||||
_assert(fragment in active_scope, f"README active roadmap missing: {fragment}")
|
||||
|
||||
forbidden_active_fragments = (
|
||||
"SCDM-first 统一实施路线",
|
||||
"SCDM-first Capability 适配路线图",
|
||||
"SCDM-first Capability 适配路线",
|
||||
"├── 6. Edge 一级编辑,补齐底层直接改边能力",
|
||||
"└── 8. 二级 / 三级关系",
|
||||
"[Face 二级传播",
|
||||
"[孔组 ->",
|
||||
"多槽组 ->",
|
||||
"二级传播 ->",
|
||||
"三级传播 ->",
|
||||
"二级 / 三级关系",
|
||||
)
|
||||
for fragment in forbidden_active_fragments:
|
||||
_assert(fragment not in active_scope, f"README 0~7 roadmap should defer this to stage 8: {fragment}")
|
||||
_assert(fragment not in active_scope, f"README SCDM-first roadmap should not keep old route scope: {fragment}")
|
||||
|
||||
_assert("[Face 二级传播 -> 孔底/槽底/台阶联动]" in deferred_scope, "README should keep Face deeper propagation in stage 8")
|
||||
_assert("0~7 不混入二级/三级传播任务" in active_scope, "README should document the active roadmap guard")
|
||||
_assert("[Analysis Situs -> 只做辅助定位、兜底识别和开源对照]" in deferred_scope, "README should keep Analysis Situs as auxiliary boundary")
|
||||
_assert("[本地 OCCT -> 只保留已验证兜底能力,不再作为新主线扩展]" in deferred_scope, "README should keep OCCT as fallback boundary")
|
||||
_assert("[SCDM 之外的新能力 -> 等 SCDM 能力适配完再评估]" in deferred_scope, "README should defer non-SCDM expansion")
|
||||
|
||||
forbidden_global_fragments = (
|
||||
"SCDM-first 统一实施路线",
|
||||
"SCDM-first Capability 适配路线图",
|
||||
"SCDM-first Capability 适配路线",
|
||||
)
|
||||
for fragment in forbidden_global_fragments:
|
||||
_assert(fragment not in readme, f"README should keep only one SCDM-first route, found old title: {fragment}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
||||
@@ -70,6 +70,7 @@ QUICK_COMMANDS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("Smoke test", ("main.py", "--smoke-test")),
|
||||
("Property editor specs", ("verify_property_editor_specs.py",)),
|
||||
("Property table editor UI", ("verify_property_card_editor_ui.py",)),
|
||||
("Relation formula rules", ("verify_relation_formula_rules.py",)),
|
||||
("Parametric component export", ("verify_parametric_component_export.py",)),
|
||||
("SCDM backend discovery", ("verify_scdm_backend.py",)),
|
||||
("SCDM runtime status", ("verify_scdm_status.py",)),
|
||||
|
||||
@@ -49,6 +49,7 @@ from step_editor.window_state import (
|
||||
RELATION_FORMULA_OBJECT_COMPLETION_LIMIT,
|
||||
WindowStateMixin,
|
||||
)
|
||||
from step_editor.ui_helpers import _selection_mode_label, _selection_mode_value
|
||||
|
||||
|
||||
class _StatusBar:
|
||||
@@ -86,7 +87,23 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
||||
self.scdm_feature_cache = None
|
||||
self.scdm_feature_cache_state = "empty"
|
||||
self.scdm_feature_cache_message = ""
|
||||
self.scdm_edit_runner_ready = {"face.offset", "hole.diameter", "hole.position", "slot.position", "boss.position"}
|
||||
self.scdm_edit_runner_ready = {
|
||||
"face.offset",
|
||||
"hole.diameter",
|
||||
"hole.position",
|
||||
"feature.fill",
|
||||
"slot.width",
|
||||
"slot.depth",
|
||||
"slot.position",
|
||||
"boss.diameter",
|
||||
"boss.height",
|
||||
"boss.position",
|
||||
"round.radius",
|
||||
"chamfer.distance",
|
||||
"feature.delete_round_or_chamfer",
|
||||
"pattern.spacing",
|
||||
"pattern.segment_spacing",
|
||||
}
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.object_edit_box = self
|
||||
@@ -1560,6 +1577,65 @@ def _assert_scdm_selection_diagnostics() -> None:
|
||||
_assert("偏移" in str(info.get("scdm_selection_enabled_capabilities")), f"SCDM enabled capability diagnostic missing: {info}")
|
||||
|
||||
|
||||
def _assert_solid_selection_does_not_expand_face_scdm_specs() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe.scdm_feature_cache_state = "ready"
|
||||
probe.scdm_edit_runner_ready = {"face.offset"}
|
||||
probe.scdm_feature_cache = {
|
||||
"objects": [
|
||||
{
|
||||
"objectId": "face:0",
|
||||
"objectType": "face",
|
||||
"geometrySignature": {"faceIds": [0], "surfaceType": "plane", "planeOffset": 0.0},
|
||||
"capabilities": [
|
||||
{
|
||||
"key": "face.offset",
|
||||
"displayName": "偏移",
|
||||
"currentValue": 0.0,
|
||||
"valueKind": "number",
|
||||
"defaultIntent": "推拉平面",
|
||||
"backendOperation": "pull_face_offset",
|
||||
"postCheck": "target_face_offset",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"objectId": "face:1",
|
||||
"objectType": "face",
|
||||
"geometrySignature": {"faceIds": [1], "surfaceType": "plane", "planeOffset": -5.0},
|
||||
"capabilities": [
|
||||
{
|
||||
"key": "face.offset",
|
||||
"displayName": "偏移",
|
||||
"currentValue": -5.0,
|
||||
"valueKind": "number",
|
||||
"defaultIntent": "推拉平面",
|
||||
"backendOperation": "pull_face_offset",
|
||||
"postCheck": "target_face_offset",
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
probe.selected_kind = "face"
|
||||
probe.selected_face_id = 0
|
||||
face_specs = probe._scdm_property_specs_for_selection()
|
||||
_assert(
|
||||
len([item for item in face_specs if item.get("scdm_capability_key") == "face.offset"]) == 1,
|
||||
f"Face selection should show the selected Face SCDM offset only: {face_specs}",
|
||||
)
|
||||
|
||||
probe.selected_kind = "solid"
|
||||
probe.selected_face_id = None
|
||||
probe.selected_solid_id = 0
|
||||
probe.model = SimpleNamespace(face_solid_ids=[0, 0])
|
||||
solid_specs = probe._scdm_property_specs_for_selection()
|
||||
_assert(
|
||||
solid_specs == [],
|
||||
f"Solid selection should not expand every child Face SCDM offset into the parameter table: {solid_specs}",
|
||||
)
|
||||
|
||||
|
||||
def _assert_operation_record_backend_sources() -> None:
|
||||
class _OperationRecordProbe(WindowActionMixin):
|
||||
pass
|
||||
@@ -1718,6 +1794,10 @@ def _assert_large_model_preload_stays_lightweight() -> None:
|
||||
_assert("scdm_local_face_signatures" in window_body, "window SCDM preload should use the lightweight model signature API")
|
||||
_assert("quick_face_info" not in window_body, "window SCDM preload must not call quick_face_info for every Face")
|
||||
loaded_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_apply_loaded_model_result")
|
||||
_assert(
|
||||
"_restore_scdm_feature_cache_from_disk()" in loaded_body,
|
||||
"STEP load should restore a matching SCDM disk cache before launching a new probe",
|
||||
)
|
||||
_assert(
|
||||
"_defer_large_model_recognition_preloads" in loaded_body,
|
||||
"large model loads should defer full external recognition preloads by default",
|
||||
@@ -1727,6 +1807,10 @@ def _assert_large_model_preload_stays_lightweight() -> None:
|
||||
"SCDM edit-result reloads should still be able to force cache refresh for validation",
|
||||
)
|
||||
preload_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_start_scdm_probe_preload")
|
||||
_assert(
|
||||
"_current_scdm_feature_cache_matches_loaded_step()" in preload_body,
|
||||
"SCDM preload should skip relaunching SpaceClaim when a current cache is already installed",
|
||||
)
|
||||
_assert(
|
||||
"local_face_signatures = self._scdm_local_face_signatures()" not in preload_body,
|
||||
"SCDM preload must not build all local Face signatures on the UI thread",
|
||||
@@ -1866,6 +1950,53 @@ def _assert_large_planar_offset_prefers_local_backend() -> None:
|
||||
_assert("SCDM" in str(probe.scdm_selection_status_message), "backend preference should explain that SCDM was bypassed")
|
||||
|
||||
|
||||
def _assert_scdm_target_values_use_backend_units() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
offset = probe._scdm_property_target_value(
|
||||
{"scdm_capability_key": "face.offset", "value_type": "number", "scdm_unit_scale": 0.001},
|
||||
"2.5",
|
||||
)
|
||||
_assert(abs(float(offset) - 0.0025) <= 1.0e-12, f"SCDM numeric targets should be converted back to backend units: {offset}")
|
||||
position = probe._scdm_property_target_value(
|
||||
{"scdm_capability_key": "hole.position", "value_type": "vector3", "scdm_unit_scale": 0.001},
|
||||
"(1, 2, 3)",
|
||||
)
|
||||
_assert(position == [0.001, 0.002, 0.003], f"SCDM vector targets should be converted back to backend units: {position}")
|
||||
spacing_spec = {"label": "阵列间距", "value_type": "positive", "max_value": 1.3571428571428572}
|
||||
_assert(
|
||||
not probe._property_target_validation_error(spacing_spec, "1.1"),
|
||||
"pattern spacing targets within the support face range should be accepted",
|
||||
)
|
||||
spacing_error = probe._property_target_validation_error(spacing_spec, "2")
|
||||
_assert(
|
||||
"阵列间距" in spacing_error and "1.35714" in spacing_error,
|
||||
f"pattern spacing beyond the support face range should be blocked in the UI: {spacing_error}",
|
||||
)
|
||||
|
||||
|
||||
def _assert_selection_mode_labels_are_english() -> None:
|
||||
expected = {
|
||||
"Feature": "Feature",
|
||||
"Face": "Face",
|
||||
"Edge": "Edge",
|
||||
"Solid": "Solid",
|
||||
"Part": "Part",
|
||||
}
|
||||
for value, label in expected.items():
|
||||
_assert(_selection_mode_label(value) == label, f"selection mode {value} should display as English: {_selection_mode_label(value)}")
|
||||
_assert(_selection_mode_value(label) == value, f"selection mode label {label} should resolve to {value}")
|
||||
for legacy, value in {
|
||||
"智能特征": "Feature",
|
||||
"特征": "Feature",
|
||||
"面": "Face",
|
||||
"边": "Edge",
|
||||
"实体": "Solid",
|
||||
"零件": "Part",
|
||||
"装配零件": "Part",
|
||||
}.items():
|
||||
_assert(_selection_mode_value(legacy) == value, f"legacy selection label {legacy} should resolve to {value}")
|
||||
|
||||
|
||||
def _assert_background_load_uses_worker() -> None:
|
||||
body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_load_step_background_or_sync")
|
||||
_assert("LoadWorker(action)" in body, "background STEP load should run through LoadWorker")
|
||||
@@ -1891,6 +2022,8 @@ def main() -> int:
|
||||
not top_level_label_probe.shown_labels,
|
||||
f"property labels were shown as transient top-level windows: {top_level_label_probe.shown_labels}",
|
||||
)
|
||||
_assert_scdm_target_values_use_backend_units()
|
||||
_assert_selection_mode_labels_are_english()
|
||||
_assert_property_table_editor(probe)
|
||||
_assert(probe.current_capability_button.text() == "软件进度", "software progress should be a compact button")
|
||||
_assert("当前支持" in probe.current_capability_button.toolTip(), "software progress button tooltip did not show supported areas")
|
||||
@@ -1952,6 +2085,7 @@ def main() -> int:
|
||||
_assert_relation_formula_ids_follow_model_remap()
|
||||
_assert_mouse_selection_guards()
|
||||
_assert_scdm_selection_diagnostics()
|
||||
_assert_solid_selection_does_not_expand_face_scdm_specs()
|
||||
_assert_operation_record_backend_sources()
|
||||
_assert_scdm_auto_prompt()
|
||||
_assert_property_ui_reroute_guards()
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from step_editor.relation_formulas import ( # noqa: E402
|
||||
RelationFormulaError,
|
||||
parse_relation_formula,
|
||||
validate_relation_formula_graph,
|
||||
)
|
||||
|
||||
|
||||
def _parse_many(texts: list[str]):
|
||||
return [parse_relation_formula(text) for text in texts]
|
||||
|
||||
|
||||
def _assert_ok(texts: list[str]) -> None:
|
||||
validate_relation_formula_graph(_parse_many(texts))
|
||||
|
||||
|
||||
def _assert_fails(texts: list[str], expected_fragment: str) -> None:
|
||||
try:
|
||||
validate_relation_formula_graph(_parse_many(texts))
|
||||
except RelationFormulaError as exc:
|
||||
message = str(exc)
|
||||
if expected_fragment not in message:
|
||||
raise AssertionError(f"expected {expected_fragment!r} in error message, got {message!r}") from exc
|
||||
return
|
||||
raise AssertionError(f"expected relation formulas to fail: {texts!r}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_assert_ok(["Face87.直径 = Face87.半径 + 0.1"])
|
||||
_assert_ok(["Face87.位置 = Face85.位置 + (0, 0, -3.5)"])
|
||||
_assert_ok(["Face85.直径 = Face87.半径", "Face11.直径 = Face85.半径"])
|
||||
|
||||
_assert_fails(["Face87.直径 = Face87.直径 + 0.1"], "不能引用自身")
|
||||
_assert_fails(["Face85.直径 = Face87.半径", "Face85.直径 = Face11.半径"], "同一目标参数")
|
||||
_assert_fails(["Face85.直径 = Face87.直径", "Face87.直径 = Face85.直径"], "循环依赖")
|
||||
_assert_fails(
|
||||
[
|
||||
"Face1.位置 = Face2.位置",
|
||||
"Face2.位置 = Face3.位置",
|
||||
"Face3.位置 = Face1.位置",
|
||||
],
|
||||
"循环依赖",
|
||||
)
|
||||
|
||||
print("relation formula rules ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -168,19 +168,40 @@ def main() -> int:
|
||||
"change_hole_diameter",
|
||||
"move_hole_axis",
|
||||
"move_slot",
|
||||
"change_slot_width",
|
||||
"change_slot_depth",
|
||||
"move_boss",
|
||||
"change_boss_height",
|
||||
"change_boss_diameter",
|
||||
"change_round_radius",
|
||||
"change_chamfer_distance",
|
||||
"change_pattern_spacing",
|
||||
"change_pattern_segment_spacing",
|
||||
"segment_before",
|
||||
"segment_split",
|
||||
"pull_face_offset",
|
||||
"fill_feature",
|
||||
"delete_round_or_chamfer",
|
||||
"StandardHoles.ModifyHoleRadius",
|
||||
"ConstantRound.ModifyRadius",
|
||||
"Chamfer.ModifyDistance",
|
||||
"OffsetFaces.Execute",
|
||||
"Move.Translate",
|
||||
"MoveOptions",
|
||||
"ConstantRoundOptions",
|
||||
"ChamferOptions",
|
||||
"OffsetFaceOptions",
|
||||
"FillOptions",
|
||||
"FillMode",
|
||||
"Delete.Execute",
|
||||
"DocumentSave",
|
||||
"scdmFaceLocators",
|
||||
"heightFaceLocators",
|
||||
"depthFaceLocators",
|
||||
"diameterFaceLocators",
|
||||
"patternInstances",
|
||||
"bodyLocators",
|
||||
"instanceKind",
|
||||
"result.json",
|
||||
"error.json",
|
||||
):
|
||||
@@ -197,6 +218,10 @@ def main() -> int:
|
||||
"faceOrdinals": [30, 31, 32],
|
||||
"globalFaceOrdinal": 40,
|
||||
"globalFaceOrdinals": [40, 41, 42],
|
||||
"width": 2.0,
|
||||
"depth": 1.5,
|
||||
"depthAxis": [0.0, 0.0, -1.0],
|
||||
"depthFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 32, "globalFaceOrdinal": 42}],
|
||||
"center": [1.0, 2.0, 3.0],
|
||||
"axis": [1.0, 0.0, 0.0],
|
||||
}
|
||||
@@ -214,6 +239,37 @@ def main() -> int:
|
||||
_assert(slot_job.get("target", {}).get("backendOperation") == "move_slot", f"slot.position should route to move_slot: {slot_job}")
|
||||
_assert(slot_job.get("target", {}).get("value") == [1.0, 2.0, 5.0], f"slot.position target should be vector3: {slot_job}")
|
||||
_assert(slot_job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [30, 31, 32], f"slot faces should be preserved: {slot_job}")
|
||||
slot_width_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "slot-width-prepared",
|
||||
backend=backend,
|
||||
capability_key="slot.width",
|
||||
target_value="2.5",
|
||||
object_id="slot:30-31-32",
|
||||
object_signature=slot_signature,
|
||||
)
|
||||
_assert(slot_width_prepared.get("ok") is True, f"slot.width should be productized and prepare an edit job: {slot_width_prepared}")
|
||||
slot_width_job = read_json(slot_width_prepared["job_path"])
|
||||
_assert(slot_width_job.get("target", {}).get("backendOperation") == "change_slot_width", f"slot.width should route to change_slot_width: {slot_width_job}")
|
||||
_assert(slot_width_job.get("target", {}).get("value") == 2.5, f"slot.width target should be numeric: {slot_width_job}")
|
||||
_assert(slot_width_job.get("object", {}).get("geometrySignature", {}).get("width") == 2.0, f"slot width signature should be preserved: {slot_width_job}")
|
||||
slot_depth_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "slot-depth-prepared",
|
||||
backend=backend,
|
||||
capability_key="slot.depth",
|
||||
target_value="2.0",
|
||||
object_id="slot:30-31-32",
|
||||
object_signature=slot_signature,
|
||||
)
|
||||
_assert(slot_depth_prepared.get("ok") is True, f"slot.depth should be productized and prepare an edit job: {slot_depth_prepared}")
|
||||
slot_depth_job = read_json(slot_depth_prepared["job_path"])
|
||||
_assert(slot_depth_job.get("target", {}).get("backendOperation") == "change_slot_depth", f"slot.depth should route to change_slot_depth: {slot_depth_job}")
|
||||
_assert(slot_depth_job.get("target", {}).get("value") == 2.0, f"slot.depth target should be numeric: {slot_depth_job}")
|
||||
slot_depth_signature = slot_depth_job.get("object", {}).get("geometrySignature", {})
|
||||
_assert(slot_depth_signature.get("depth") == 1.5, f"slot depth signature should be preserved: {slot_depth_job}")
|
||||
_assert(slot_depth_signature.get("depthFaceLocators"), f"slot depth bottom face locator should be preserved: {slot_depth_job}")
|
||||
_assert(slot_depth_signature.get("depthAxis") == [0.0, 0.0, -1.0], f"slot depth axis should be preserved: {slot_depth_job}")
|
||||
|
||||
boss_signature = {
|
||||
"objectType": "cylindrical_boss",
|
||||
@@ -223,6 +279,10 @@ def main() -> int:
|
||||
"faceOrdinals": [50, 51, 52],
|
||||
"globalFaceOrdinal": 70,
|
||||
"globalFaceOrdinals": [70, 71, 72],
|
||||
"height": 4.0,
|
||||
"diameter": 3.0,
|
||||
"heightFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 52, "globalFaceOrdinal": 72}],
|
||||
"diameterFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 50, "globalFaceOrdinal": 70}],
|
||||
"center": [0.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
}
|
||||
@@ -240,12 +300,369 @@ def main() -> int:
|
||||
_assert(boss_job.get("target", {}).get("backendOperation") == "move_boss", f"boss.position should route to move_boss: {boss_job}")
|
||||
_assert(boss_job.get("target", {}).get("value") == [2.0, 0.0, 2.0], f"boss.position target should be vector3: {boss_job}")
|
||||
_assert(boss_job.get("object", {}).get("geometrySignature", {}).get("faceIds") == [50, 51, 52], f"boss faces should be preserved: {boss_job}")
|
||||
boss_height_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "boss-height-prepared",
|
||||
backend=backend,
|
||||
capability_key="boss.height",
|
||||
target_value="5.5",
|
||||
object_id="boss:50-51-52",
|
||||
object_signature=boss_signature,
|
||||
)
|
||||
_assert(boss_height_prepared.get("ok") is True, f"boss.height should be productized and prepare an edit job: {boss_height_prepared}")
|
||||
boss_height_job = read_json(boss_height_prepared["job_path"])
|
||||
_assert(boss_height_job.get("target", {}).get("backendOperation") == "change_boss_height", f"boss.height should route to change_boss_height: {boss_height_job}")
|
||||
_assert(boss_height_job.get("target", {}).get("value") == 5.5, f"boss.height target should be numeric: {boss_height_job}")
|
||||
boss_height_signature = boss_height_job.get("object", {}).get("geometrySignature", {})
|
||||
_assert(boss_height_signature.get("height") == 4.0, f"boss height signature should be preserved: {boss_height_job}")
|
||||
_assert(boss_height_signature.get("heightFaceLocators"), f"boss height top face locator should be preserved: {boss_height_job}")
|
||||
boss_diameter_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "boss-diameter-prepared",
|
||||
backend=backend,
|
||||
capability_key="boss.diameter",
|
||||
target_value="4.5",
|
||||
object_id="boss:50-51-52",
|
||||
object_signature=boss_signature,
|
||||
)
|
||||
_assert(boss_diameter_prepared.get("ok") is True, f"boss.diameter should be productized and prepare an edit job: {boss_diameter_prepared}")
|
||||
boss_diameter_job = read_json(boss_diameter_prepared["job_path"])
|
||||
_assert(boss_diameter_job.get("target", {}).get("backendOperation") == "change_boss_diameter", f"boss.diameter should route to change_boss_diameter: {boss_diameter_job}")
|
||||
_assert(boss_diameter_job.get("target", {}).get("value") == 4.5, f"boss.diameter target should be numeric: {boss_diameter_job}")
|
||||
boss_diameter_signature = boss_diameter_job.get("object", {}).get("geometrySignature", {})
|
||||
_assert(boss_diameter_signature.get("diameter") == 3.0, f"boss diameter signature should be preserved: {boss_diameter_job}")
|
||||
_assert(boss_diameter_signature.get("diameterFaceLocators"), f"boss diameter side face locator should be preserved: {boss_diameter_job}")
|
||||
|
||||
round_signature = {
|
||||
"objectType": "round",
|
||||
"faceIds": [60],
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 60,
|
||||
"globalFaceOrdinal": 80,
|
||||
"center": [1.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"radius": 0.5,
|
||||
"roundType": "ConstantRound",
|
||||
"isConstantRound": True,
|
||||
}
|
||||
round_radius_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "round-radius-prepared",
|
||||
backend=backend,
|
||||
capability_key="round.radius",
|
||||
target_value="0.75",
|
||||
object_id="round:60",
|
||||
object_signature=round_signature,
|
||||
)
|
||||
_assert(round_radius_prepared.get("ok") is True, f"round.radius should be productized and prepare an edit job: {round_radius_prepared}")
|
||||
round_radius_job = read_json(round_radius_prepared["job_path"])
|
||||
_assert(round_radius_job.get("target", {}).get("backendOperation") == "change_round_radius", f"round.radius should route to change_round_radius: {round_radius_job}")
|
||||
_assert(round_radius_job.get("target", {}).get("value") == 0.75, f"round.radius target should be numeric: {round_radius_job}")
|
||||
round_radius_signature = round_radius_job.get("object", {}).get("geometrySignature", {})
|
||||
_assert(round_radius_signature.get("isConstantRound") is True, f"round.radius should preserve constant round evidence: {round_radius_job}")
|
||||
_assert(round_radius_signature.get("radius") == 0.5, f"round.radius should preserve current radius: {round_radius_job}")
|
||||
round_delete_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "round-delete-prepared",
|
||||
backend=backend,
|
||||
capability_key="feature.delete_round_or_chamfer",
|
||||
target_value="",
|
||||
object_id="round:60",
|
||||
object_signature=round_signature,
|
||||
)
|
||||
_assert(round_delete_prepared.get("ok") is True, f"round/chamfer delete should be productized and prepare an edit job: {round_delete_prepared}")
|
||||
round_delete_job = read_json(round_delete_prepared["job_path"])
|
||||
_assert(round_delete_job.get("target", {}).get("backendOperation") == "delete_round_or_chamfer", f"round delete should route to delete_round_or_chamfer: {round_delete_job}")
|
||||
_assert(round_delete_job.get("target", {}).get("value") is True, f"command target should be true: {round_delete_job}")
|
||||
|
||||
chamfer_signature = {
|
||||
"objectType": "chamfer",
|
||||
"faceIds": [63],
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 63,
|
||||
"globalFaceOrdinal": 83,
|
||||
"distance": 0.8,
|
||||
"distance1": 0.8,
|
||||
"distance2": 0.8,
|
||||
"chamferType": "EqualDistanceChamfer",
|
||||
"isEqualDistanceChamfer": True,
|
||||
}
|
||||
chamfer_distance_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "chamfer-distance-prepared",
|
||||
backend=backend,
|
||||
capability_key="chamfer.distance",
|
||||
target_value="1.2",
|
||||
object_id="chamfer:63",
|
||||
object_signature=chamfer_signature,
|
||||
)
|
||||
_assert(chamfer_distance_prepared.get("ok") is True, f"chamfer.distance should be productized and prepare an edit job: {chamfer_distance_prepared}")
|
||||
chamfer_distance_job = read_json(chamfer_distance_prepared["job_path"])
|
||||
_assert(chamfer_distance_job.get("target", {}).get("backendOperation") == "change_chamfer_distance", f"chamfer.distance should route to change_chamfer_distance: {chamfer_distance_job}")
|
||||
_assert(chamfer_distance_job.get("target", {}).get("value") == 1.2, f"chamfer.distance target should be numeric: {chamfer_distance_job}")
|
||||
chamfer_job_signature = chamfer_distance_job.get("object", {}).get("geometrySignature", {})
|
||||
_assert(chamfer_job_signature.get("isEqualDistanceChamfer") is True, f"chamfer.distance should preserve equal-distance evidence: {chamfer_distance_job}")
|
||||
_assert(chamfer_job_signature.get("distance") == 0.8, f"chamfer.distance should preserve current distance: {chamfer_distance_job}")
|
||||
|
||||
pattern_signature = {
|
||||
"objectType": "linear_pattern",
|
||||
"faceIds": [85, 94, 87, 96, 89, 98],
|
||||
"bodyIndex": 0,
|
||||
"axis": [1.0, 0.0, 0.0],
|
||||
"spacing": 5.0,
|
||||
"pitch": 5.0,
|
||||
"instanceCount": 3,
|
||||
"instanceCenters": [[0.0, 0.0, 0.0], [5.0, 0.0, 0.0], [10.0, 0.0, 0.0]],
|
||||
"patternInstances": [
|
||||
{
|
||||
"sourceObjectId": "hole:a",
|
||||
"center": [0.0, 0.0, 0.0],
|
||||
"faceIds": [85, 94],
|
||||
"scdmFaceLocators": [
|
||||
{"bodyIndex": 0, "faceOrdinal": 12, "globalFaceOrdinal": 85},
|
||||
{"bodyIndex": 0, "faceOrdinal": 19, "globalFaceOrdinal": 94},
|
||||
],
|
||||
},
|
||||
{
|
||||
"sourceObjectId": "hole:b",
|
||||
"center": [5.0, 0.0, 0.0],
|
||||
"faceIds": [87, 96],
|
||||
"scdmFaceLocators": [
|
||||
{"bodyIndex": 0, "faceOrdinal": 22, "globalFaceOrdinal": 87},
|
||||
{"bodyIndex": 0, "faceOrdinal": 29, "globalFaceOrdinal": 96},
|
||||
],
|
||||
},
|
||||
{
|
||||
"sourceObjectId": "hole:c",
|
||||
"center": [10.0, 0.0, 0.0],
|
||||
"faceIds": [89, 98],
|
||||
"scdmFaceLocators": [
|
||||
{"bodyIndex": 0, "faceOrdinal": 32, "globalFaceOrdinal": 89},
|
||||
{"bodyIndex": 0, "faceOrdinal": 39, "globalFaceOrdinal": 98},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
pattern_spacing_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-spacing-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.spacing",
|
||||
target_value="7.5",
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=pattern_signature,
|
||||
)
|
||||
_assert(pattern_spacing_prepared.get("ok") is True, f"pattern.spacing should be productized and prepare an edit job: {pattern_spacing_prepared}")
|
||||
pattern_spacing_job = read_json(pattern_spacing_prepared["job_path"])
|
||||
_assert(pattern_spacing_job.get("target", {}).get("backendOperation") == "change_pattern_spacing", f"pattern.spacing should route to change_pattern_spacing: {pattern_spacing_job}")
|
||||
_assert(pattern_spacing_job.get("target", {}).get("value") == 7.5, f"pattern.spacing target should be numeric: {pattern_spacing_job}")
|
||||
pattern_job_signature = pattern_spacing_job.get("object", {}).get("geometrySignature", {})
|
||||
_assert(pattern_job_signature.get("spacing") == 5.0, f"pattern spacing signature should be preserved: {pattern_spacing_job}")
|
||||
_assert(len(pattern_job_signature.get("patternInstances") or []) == 3, f"pattern instance locators should be preserved: {pattern_spacing_job}")
|
||||
pattern_segment_signature = dict(pattern_signature)
|
||||
pattern_segment_signature["segmentIndex"] = 1
|
||||
pattern_segment_signature["movingSide"] = "after"
|
||||
pattern_segment_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-segment-spacing-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value="6.5",
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=pattern_segment_signature,
|
||||
)
|
||||
_assert(pattern_segment_prepared.get("ok") is True, f"pattern.segment_spacing should prepare an edit job: {pattern_segment_prepared}")
|
||||
pattern_segment_job = read_json(pattern_segment_prepared["job_path"])
|
||||
_assert(
|
||||
pattern_segment_job.get("target", {}).get("backendOperation") == "change_pattern_segment_spacing",
|
||||
f"pattern.segment_spacing should route to change_pattern_segment_spacing: {pattern_segment_job}",
|
||||
)
|
||||
_assert(pattern_segment_job.get("target", {}).get("value") == 6.5, f"pattern.segment_spacing target should be numeric: {pattern_segment_job}")
|
||||
_assert(
|
||||
pattern_segment_job.get("object", {}).get("geometrySignature", {}).get("segmentIndex") == 1,
|
||||
f"pattern.segment_spacing should preserve the selected adjacent segment: {pattern_segment_job}",
|
||||
)
|
||||
pattern_segment_before_signature = dict(pattern_segment_signature)
|
||||
pattern_segment_before_signature["movingSide"] = "before"
|
||||
pattern_segment_before_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-segment-spacing-before-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value="6.5",
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=pattern_segment_before_signature,
|
||||
)
|
||||
_assert(
|
||||
pattern_segment_before_prepared.get("ok") is True
|
||||
and read_json(pattern_segment_before_prepared["job_path"]).get("object", {}).get("geometrySignature", {}).get("movingSide") == "before",
|
||||
f"pattern.segment_spacing should preserve fix-right/move-left semantics: {pattern_segment_before_prepared}",
|
||||
)
|
||||
pattern_segment_split_signature = dict(pattern_segment_signature)
|
||||
pattern_segment_split_signature["movingSide"] = "split"
|
||||
pattern_segment_split_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-segment-spacing-split-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value="6.5",
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=pattern_segment_split_signature,
|
||||
)
|
||||
_assert(
|
||||
pattern_segment_split_prepared.get("ok") is True
|
||||
and read_json(pattern_segment_split_prepared["job_path"]).get("object", {}).get("geometrySignature", {}).get("movingSide") == "split",
|
||||
f"pattern.segment_spacing should preserve split/keep-center semantics: {pattern_segment_split_prepared}",
|
||||
)
|
||||
|
||||
body_pattern_signature = {
|
||||
"objectType": "linear_pattern",
|
||||
"patternKind": "body",
|
||||
"instanceKind": "body",
|
||||
"faceIds": [300, 301, 302],
|
||||
"bodyIndices": [20, 21, 22],
|
||||
"axis": [0.0, 1.0, 0.0],
|
||||
"spacing": 5.0,
|
||||
"pitch": 5.0,
|
||||
"instanceCount": 3,
|
||||
"instanceCenters": [[0.0, 20.0, 0.0], [0.0, 25.0, 0.0], [0.0, 30.0, 0.0]],
|
||||
"patternInstances": [
|
||||
{"sourceObjectId": "body:20", "instanceKind": "body", "center": [0.0, 20.0, 0.0], "bodyIndex": 20, "bodyLocators": [{"bodyIndex": 20}], "faceIds": [300]},
|
||||
{"sourceObjectId": "body:21", "instanceKind": "body", "center": [0.0, 25.0, 0.0], "bodyIndex": 21, "bodyLocators": [{"bodyIndex": 21}], "faceIds": [301]},
|
||||
{"sourceObjectId": "body:22", "instanceKind": "body", "center": [0.0, 30.0, 0.0], "bodyIndex": 22, "bodyLocators": [{"bodyIndex": 22}], "faceIds": [302]},
|
||||
],
|
||||
}
|
||||
body_pattern_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "body-pattern-spacing-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.spacing",
|
||||
target_value="8",
|
||||
object_id="pattern:parts-20-22",
|
||||
object_signature=body_pattern_signature,
|
||||
)
|
||||
_assert(
|
||||
body_pattern_prepared.get("ok") is False
|
||||
and body_pattern_prepared.get("reason") == "body-pattern-spacing-missing-component-locators",
|
||||
f"body pattern spacing should be blocked until component occurrence locators are available: {body_pattern_prepared}",
|
||||
)
|
||||
body_segment_pattern_signature = dict(body_pattern_signature)
|
||||
body_segment_pattern_signature["segmentIndex"] = 0
|
||||
body_segment_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "body-pattern-segment-spacing-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value="6",
|
||||
object_id="pattern:parts-20-22",
|
||||
object_signature=body_segment_pattern_signature,
|
||||
)
|
||||
_assert(
|
||||
body_segment_prepared.get("ok") is False
|
||||
and body_segment_prepared.get("reason") == "body-pattern-spacing-missing-component-locators",
|
||||
f"body pattern local segment spacing should also require component occurrence locators: {body_segment_prepared}",
|
||||
)
|
||||
component_body_pattern_signature = dict(body_pattern_signature)
|
||||
component_body_pattern_signature["componentInstanceCount"] = 3
|
||||
component_body_pattern_signature["patternInstances"] = [
|
||||
{
|
||||
**dict(item),
|
||||
"componentLocators": [
|
||||
{
|
||||
"componentIndex": index,
|
||||
"componentPath": [index],
|
||||
"componentBodyIndex": 0,
|
||||
"bodyIndex": item.get("bodyIndex"),
|
||||
"componentName": f"Part {index + 1}",
|
||||
}
|
||||
],
|
||||
}
|
||||
for index, item in enumerate(body_pattern_signature["patternInstances"])
|
||||
]
|
||||
component_body_pattern_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "component-body-pattern-spacing-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.spacing",
|
||||
target_value="8",
|
||||
object_id="pattern:components-20-22",
|
||||
object_signature=component_body_pattern_signature,
|
||||
)
|
||||
_assert(
|
||||
component_body_pattern_prepared.get("ok") is True,
|
||||
f"body pattern spacing should prepare when every member has a component occurrence locator: {component_body_pattern_prepared}",
|
||||
)
|
||||
component_body_pattern_job = read_json(component_body_pattern_prepared["job_path"])
|
||||
component_instances = component_body_pattern_job.get("object", {}).get("geometrySignature", {}).get("patternInstances") or []
|
||||
_assert(
|
||||
all(item.get("componentLocators") for item in component_instances if isinstance(item, dict)),
|
||||
f"component occurrence locators should be preserved in pattern.spacing job: {component_body_pattern_job}",
|
||||
)
|
||||
component_body_segment_signature = dict(component_body_pattern_signature)
|
||||
component_body_segment_signature["segmentIndex"] = 0
|
||||
component_body_segment_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "component-body-pattern-segment-spacing-prepared",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value="6",
|
||||
object_id="pattern:components-20-22",
|
||||
object_signature=component_body_segment_signature,
|
||||
)
|
||||
_assert(
|
||||
component_body_segment_prepared.get("ok") is True,
|
||||
f"body pattern local segment spacing should prepare with component occurrence locators: {component_body_segment_prepared}",
|
||||
)
|
||||
blocked_pattern_signature = dict(pattern_signature)
|
||||
blocked_pattern_signature["supportPatternFit"] = {
|
||||
"supportFaceIds": [92],
|
||||
"localUnitScale": 0.001,
|
||||
"maxSpacing": 0.0012,
|
||||
"maxSpacingLocal": 1.2,
|
||||
"instanceCount": 3,
|
||||
}
|
||||
blocked_pattern_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "body-pattern-spacing-blocked",
|
||||
backend=backend,
|
||||
capability_key="pattern.spacing",
|
||||
target_value=0.002,
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=blocked_pattern_signature,
|
||||
)
|
||||
_assert(
|
||||
blocked_pattern_prepared.get("ok") is False
|
||||
and blocked_pattern_prepared.get("reason") == "pattern-spacing-exceeds-support",
|
||||
f"pattern spacing should be blocked before SCDM when it exceeds support Face limits: {blocked_pattern_prepared}",
|
||||
)
|
||||
blocked_segment_signature = dict(pattern_segment_signature)
|
||||
blocked_segment_signature["supportPatternFit"] = {
|
||||
"supportFaceIds": [92],
|
||||
"localUnitScale": 0.001,
|
||||
"maxSegmentSpacing": 0.0013,
|
||||
"maxSegmentSpacingLocal": 1.3,
|
||||
"instanceCount": 3,
|
||||
}
|
||||
blocked_segment_prepared = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-segment-spacing-blocked",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value=0.002,
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=blocked_segment_signature,
|
||||
)
|
||||
_assert(
|
||||
blocked_segment_prepared.get("ok") is False
|
||||
and blocked_segment_prepared.get("reason") == "pattern-spacing-exceeds-support",
|
||||
f"pattern segment spacing should be blocked before SCDM when it exceeds support Face limits: {blocked_segment_prepared}",
|
||||
)
|
||||
|
||||
planned = prepare_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "unsupported",
|
||||
backend=backend,
|
||||
capability_key="slot.width",
|
||||
capability_key="pattern.instance_position",
|
||||
target_value="1",
|
||||
object_signature=signature,
|
||||
)
|
||||
@@ -305,6 +722,30 @@ def main() -> int:
|
||||
)
|
||||
_assert(slot_success.get("ok") is True, f"fake slot.position edit should succeed: {slot_success}")
|
||||
_assert(slot_success.get("backend_operation") == "move_slot", f"slot.position should report move_slot: {slot_success}")
|
||||
slot_width_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "slot-width-success",
|
||||
backend=backend,
|
||||
capability_key="slot.width",
|
||||
target_value=2.5,
|
||||
object_id="slot:30-31-32",
|
||||
object_signature=slot_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(slot_width_success.get("ok") is True, f"fake slot.width edit should succeed: {slot_width_success}")
|
||||
_assert(slot_width_success.get("backend_operation") == "change_slot_width", f"slot.width should report change_slot_width: {slot_width_success}")
|
||||
slot_depth_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "slot-depth-success",
|
||||
backend=backend,
|
||||
capability_key="slot.depth",
|
||||
target_value=2.0,
|
||||
object_id="slot:30-31-32",
|
||||
object_signature=slot_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(slot_depth_success.get("ok") is True, f"fake slot.depth edit should succeed: {slot_depth_success}")
|
||||
_assert(slot_depth_success.get("backend_operation") == "change_slot_depth", f"slot.depth should report change_slot_depth: {slot_depth_success}")
|
||||
|
||||
boss_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
@@ -318,6 +759,97 @@ def main() -> int:
|
||||
)
|
||||
_assert(boss_success.get("ok") is True, f"fake boss.position edit should succeed: {boss_success}")
|
||||
_assert(boss_success.get("backend_operation") == "move_boss", f"boss.position should report move_boss: {boss_success}")
|
||||
boss_height_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "boss-height-success",
|
||||
backend=backend,
|
||||
capability_key="boss.height",
|
||||
target_value=5.5,
|
||||
object_id="boss:50-51-52",
|
||||
object_signature=boss_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(boss_height_success.get("ok") is True, f"fake boss.height edit should succeed: {boss_height_success}")
|
||||
_assert(boss_height_success.get("backend_operation") == "change_boss_height", f"boss.height should report change_boss_height: {boss_height_success}")
|
||||
boss_diameter_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "boss-diameter-success",
|
||||
backend=backend,
|
||||
capability_key="boss.diameter",
|
||||
target_value=4.5,
|
||||
object_id="boss:50-51-52",
|
||||
object_signature=boss_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(boss_diameter_success.get("ok") is True, f"fake boss.diameter edit should succeed: {boss_diameter_success}")
|
||||
_assert(boss_diameter_success.get("backend_operation") == "change_boss_diameter", f"boss.diameter should report change_boss_diameter: {boss_diameter_success}")
|
||||
|
||||
round_radius_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "round-radius-success",
|
||||
backend=backend,
|
||||
capability_key="round.radius",
|
||||
target_value=0.75,
|
||||
object_id="round:60",
|
||||
object_signature=round_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(round_radius_success.get("ok") is True, f"fake round.radius edit should succeed: {round_radius_success}")
|
||||
_assert(round_radius_success.get("backend_operation") == "change_round_radius", f"round.radius should report change_round_radius: {round_radius_success}")
|
||||
|
||||
round_delete_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "round-delete-success",
|
||||
backend=backend,
|
||||
capability_key="feature.delete_round_or_chamfer",
|
||||
target_value="",
|
||||
object_id="round:60",
|
||||
object_signature=round_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(round_delete_success.get("ok") is True, f"fake round/chamfer delete edit should succeed: {round_delete_success}")
|
||||
_assert(round_delete_success.get("backend_operation") == "delete_round_or_chamfer", f"round/chamfer delete should report delete_round_or_chamfer: {round_delete_success}")
|
||||
|
||||
chamfer_distance_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "chamfer-distance-success",
|
||||
backend=backend,
|
||||
capability_key="chamfer.distance",
|
||||
target_value=1.2,
|
||||
object_id="chamfer:63",
|
||||
object_signature=chamfer_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(chamfer_distance_success.get("ok") is True, f"fake chamfer.distance edit should succeed: {chamfer_distance_success}")
|
||||
_assert(chamfer_distance_success.get("backend_operation") == "change_chamfer_distance", f"chamfer.distance should report change_chamfer_distance: {chamfer_distance_success}")
|
||||
|
||||
pattern_spacing_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-spacing-success",
|
||||
backend=backend,
|
||||
capability_key="pattern.spacing",
|
||||
target_value=7.5,
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=pattern_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(pattern_spacing_success.get("ok") is True, f"fake pattern.spacing edit should succeed: {pattern_spacing_success}")
|
||||
_assert(pattern_spacing_success.get("backend_operation") == "change_pattern_spacing", f"pattern.spacing should report change_pattern_spacing: {pattern_spacing_success}")
|
||||
pattern_segment_success = run_scdm_edit_job(
|
||||
step_path,
|
||||
output_dir=root / "pattern-segment-spacing-success",
|
||||
backend=backend,
|
||||
capability_key="pattern.segment_spacing",
|
||||
target_value=6.5,
|
||||
object_id="pattern:holes-a-b-c",
|
||||
object_signature=pattern_segment_signature,
|
||||
runner=_successful_runner,
|
||||
)
|
||||
_assert(pattern_segment_success.get("ok") is True, f"fake pattern.segment_spacing edit should succeed: {pattern_segment_success}")
|
||||
_assert(
|
||||
pattern_segment_success.get("backend_operation") == "change_pattern_segment_spacing",
|
||||
f"pattern.segment_spacing should report change_pattern_segment_spacing: {pattern_segment_success}",
|
||||
)
|
||||
|
||||
missing_output = run_scdm_edit_job(
|
||||
step_path,
|
||||
|
||||
@@ -95,11 +95,15 @@ def main() -> int:
|
||||
_assert("StandardHoles" in script and "FindStandardHoleOptions" in script and "getattr(standard_holes, 'Find'" in script, "probe script should try SCDM StandardHoles.Find before geometric fallback")
|
||||
_assert("availableCommands" in script and "ConstantRound" in script and "Chamfer" in script, "probe script should report SCDM command availability")
|
||||
_assert("RoundInfo" in script and "_round_info_from_face" in script and "change_round_radius" in script, "probe script should collect SCDM round diagnostics")
|
||||
_assert("ChamferInfo" in script and "_chamfer_info_from_face" in script and "change_chamfer_distance" in script, "probe script should collect SCDM chamfer diagnostics")
|
||||
_assert("SlotInfo" in script and "_slot_info_from_face" in script and "change_slot_depth" in script, "probe script should collect SCDM slot diagnostics")
|
||||
_assert("backendCommandCandidates" in script, "probe script should write raw command candidates")
|
||||
for token in ("_geometry_from_edge", "Length", "StartPoint", "EndPoint", "adjacentFaceOrdinals"):
|
||||
_assert(token in script, f"probe script should enrich Edge raw geometry with {token}")
|
||||
for token in ("_record_face_adjacency", "_face_adjacency_rows", "edgeGeometrySummary", "_final_edge_geometry_summary", "_feature_inventory"):
|
||||
_assert(token in script, f"probe script should summarize SCDM topology evidence with {token}")
|
||||
for token in ("_component_entries", "_component_body_locator_map", "componentInstances", "componentLocators"):
|
||||
_assert(token in script, f"probe script should preserve SCDM component occurrence evidence with {token}")
|
||||
generated = generate_scdm_probe_script(job_path)
|
||||
_assert("JOB_PATH =" in generated and job_path.name in generated, "generated probe script should embed the job path")
|
||||
|
||||
@@ -123,7 +127,10 @@ def main() -> int:
|
||||
{"name": "Move", "available": True},
|
||||
{"name": "Fill", "available": True},
|
||||
{"name": "Delete", "available": True},
|
||||
{"name": "Chamfer", "available": True},
|
||||
{"name": "ConstantRound", "available": True},
|
||||
{"name": "ChamferInfo", "available": True},
|
||||
{"name": "SlotInfo", "available": True},
|
||||
{"name": "SomeFutureCommand", "available": False},
|
||||
],
|
||||
"faceAdjacency": [
|
||||
@@ -144,18 +151,30 @@ def main() -> int:
|
||||
"maxEdgeLength": 8.0,
|
||||
},
|
||||
"featureInventory": {
|
||||
"objectTypeCounts": {"face": 2, "hole": 1, "edge": 1, "slot": 1, "round": 1},
|
||||
"objectTypeCounts": {"face": 2, "hole": 1, "edge": 1, "slot": 1, "round": 1, "chamfer": 1},
|
||||
"surfaceTypeCounts": {"plane": 1, "cylinder": 3},
|
||||
"curveTypeCounts": {"Line": 5, "Circle": 4},
|
||||
"operationCounts": {
|
||||
"pull_face_offset": 1,
|
||||
"change_hole_diameter": 1,
|
||||
"change_slot_width": 1,
|
||||
"change_slot_depth": 1,
|
||||
"move_slot": 1,
|
||||
"change_boss_height": 1,
|
||||
"move_boss": 1,
|
||||
"change_chamfer_distance": 1,
|
||||
},
|
||||
},
|
||||
"componentInstances": [
|
||||
{
|
||||
"backendId": "component:0",
|
||||
"componentIndex": 0,
|
||||
"componentPath": [0],
|
||||
"componentName": "零件 3",
|
||||
"contentBodyCount": 1,
|
||||
"placementTranslation": [0.0, 20.0, 0.0],
|
||||
}
|
||||
],
|
||||
},
|
||||
"summary": {"bodyCount": 1, "objectCount": 10, "faceCount": 6, "edgeCount": 1, "holeFaceCount": 0},
|
||||
"objects": [
|
||||
@@ -233,6 +252,34 @@ def main() -> int:
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:2/face:100",
|
||||
"objectType": "face",
|
||||
"geometry": {
|
||||
"surfaceType": "plane",
|
||||
"center": [0.0, 0.0, 0.0],
|
||||
"normal": [0.0, 0.0, 1.0],
|
||||
},
|
||||
"topologyHint": {"faceIds": [100], "bodyIndex": 2, "faceOrdinal": 100, "globalFaceOrdinal": 100},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "pull_face_offset", "enabled": True, "parameterFields": {"distance": 0.0}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:2/face:101",
|
||||
"objectType": "face",
|
||||
"geometry": {
|
||||
"surfaceType": "plane",
|
||||
"center": [0.0, 0.0, 1.2],
|
||||
"normal": [0.0, 0.0, -1.0],
|
||||
},
|
||||
"topologyHint": {"faceIds": [101], "bodyIndex": 2, "faceOrdinal": 101, "globalFaceOrdinal": 101},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "pull_face_offset", "enabled": True, "parameterFields": {"distance": 0.0}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:8/face:5",
|
||||
"objectType": "face",
|
||||
@@ -262,7 +309,17 @@ def main() -> int:
|
||||
{
|
||||
"backendId": "round:1",
|
||||
"objectType": "round",
|
||||
"geometry": {"radius": 1.0},
|
||||
"geometry": {
|
||||
"radius": 1.0,
|
||||
"roundInfo": {
|
||||
"radius": 1.0,
|
||||
"diameter": 2.0,
|
||||
"isConstant": True,
|
||||
"isRound": True,
|
||||
"type": "ConstantRound",
|
||||
},
|
||||
},
|
||||
"topologyHint": {"faceIds": [60], "bodyIndex": 0, "faceOrdinal": 60, "globalFaceOrdinal": 60},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_round_radius", "enabled": True},
|
||||
{"operation": "delete_round_or_chamfer", "enabled": True},
|
||||
@@ -272,10 +329,16 @@ def main() -> int:
|
||||
{
|
||||
"backendId": "slot:1",
|
||||
"objectType": "slot",
|
||||
"geometry": {"width": 2.0, "depth": 1.5, "center": [1.0, 2.0, 3.0]},
|
||||
"topologyHint": {"faceIds": [30, 31, 32], "bodyIndex": 0, "faceOrdinal": 30},
|
||||
"geometry": {"width": 2.0, "depth": 1.5, "depthAxis": [0.0, 0.0, -1.0], "center": [1.0, 2.0, 3.0]},
|
||||
"topologyHint": {
|
||||
"faceIds": [30, 31, 32],
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 30,
|
||||
"depthFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 32, "globalFaceOrdinal": 42}],
|
||||
},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_slot_width", "enabled": True},
|
||||
{"operation": "change_slot_depth", "enabled": True},
|
||||
{"operation": "move_slot", "enabled": True, "parameterFields": {"center": [1.0, 2.0, 3.0]}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
@@ -284,9 +347,16 @@ def main() -> int:
|
||||
"backendId": "boss:1",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometry": {"diameter": 3.0, "height": 4.0, "center": [0.0, 0.0, 2.0]},
|
||||
"topologyHint": {"faceIds": [50, 51, 52], "bodyIndex": 0, "faceOrdinal": 50},
|
||||
"topologyHint": {
|
||||
"faceIds": [50, 51, 52],
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 50,
|
||||
"heightFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 52, "globalFaceOrdinal": 72}],
|
||||
"diameterFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 50, "globalFaceOrdinal": 70}],
|
||||
},
|
||||
"backendCommandCandidates": [
|
||||
{"operation": "change_boss_height", "enabled": True},
|
||||
{"operation": "change_boss_diameter", "enabled": True},
|
||||
{"operation": "move_boss", "enabled": True, "parameterFields": {"center": [0.0, 0.0, 2.0]}},
|
||||
],
|
||||
"rawLimitations": [],
|
||||
@@ -294,7 +364,18 @@ def main() -> int:
|
||||
{
|
||||
"backendId": "chamfer:1",
|
||||
"objectType": "chamfer",
|
||||
"geometry": {"distance": 0.8},
|
||||
"geometry": {
|
||||
"distance": 0.8,
|
||||
"chamferInfo": {
|
||||
"distance": 0.8,
|
||||
"distance1": 0.8,
|
||||
"distance2": 0.8,
|
||||
"isEqualDistance": True,
|
||||
"isChamfer": True,
|
||||
"type": "EqualDistanceChamfer",
|
||||
},
|
||||
},
|
||||
"topologyHint": {"faceIds": [63], "bodyIndex": 0, "faceOrdinal": 63, "globalFaceOrdinal": 63},
|
||||
"backendCommandCandidates": [{"operation": "change_chamfer_distance", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
@@ -305,6 +386,48 @@ def main() -> int:
|
||||
"backendCommandCandidates": [{"operation": "change_pattern_spacing", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:20",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 20.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {
|
||||
"bodyIndex": 20,
|
||||
"faceIds": [300],
|
||||
"faceOrdinals": [0],
|
||||
"globalFaceOrdinals": [300],
|
||||
"componentLocators": [{"componentIndex": 0, "componentPath": [0], "componentBodyIndex": 0, "bodyIndex": 20}],
|
||||
},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:21",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 25.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {
|
||||
"bodyIndex": 21,
|
||||
"faceIds": [301],
|
||||
"faceOrdinals": [0],
|
||||
"globalFaceOrdinals": [301],
|
||||
"componentLocators": [{"componentIndex": 1, "componentPath": [1], "componentBodyIndex": 0, "bodyIndex": 21}],
|
||||
},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:22",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 30.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {
|
||||
"bodyIndex": 22,
|
||||
"faceIds": [302],
|
||||
"faceOrdinals": [0],
|
||||
"globalFaceOrdinals": [302],
|
||||
"componentLocators": [{"componentIndex": 2, "componentPath": [2], "componentBodyIndex": 0, "bodyIndex": 22}],
|
||||
},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "shell:1",
|
||||
"objectType": "shell",
|
||||
@@ -342,7 +465,21 @@ def main() -> int:
|
||||
_assert(edge_signature.get("length") == 3.14, f"Edge length should be preserved: {edge_signature}")
|
||||
_assert(edge_signature.get("startPoint") == [0.0, 0.0, 0.0], f"Edge start point should be preserved: {edge_signature}")
|
||||
_assert(edge_signature.get("adjacentFaceOrdinals") == [3, 4], f"Edge adjacent faces should be preserved: {edge_signature}")
|
||||
component_signature = geometry_signature(
|
||||
{
|
||||
"backendId": "body:20",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 20.0, 0.0]},
|
||||
"topologyHint": {
|
||||
"bodyIndex": 20,
|
||||
"bodyLocators": [{"bodyIndex": 20, "componentIndex": 0, "componentPath": [0], "componentBodyIndex": 0}],
|
||||
"componentLocators": [{"componentIndex": 0, "componentPath": [0], "componentBodyIndex": 0, "bodyIndex": 20}],
|
||||
},
|
||||
}
|
||||
)
|
||||
_assert(component_signature.get("componentLocators"), f"component locators should be preserved in geometry signatures: {component_signature}")
|
||||
_assert(cache.get("modelFingerprint") == "abc123", f"model fingerprint should be copied: {cache}")
|
||||
_assert(int(cache.get("mapperRevision") or 0) >= 2, f"SCDM cache should carry the mapper revision for disk reuse: {cache}")
|
||||
_assert(cache.get("backendVersion") == "v222", f"backend version should be copied: {cache}")
|
||||
_assert({"hole.diameter", "hole.position"} <= _capability_keys(cache, "hole:1"), f"hole caps missing: {cache}")
|
||||
objects = cache.get("objects")
|
||||
@@ -375,24 +512,63 @@ def main() -> int:
|
||||
_assert({str(spec.get("scdm_capability_key")) for spec in face_specs} == {"face.offset"}, f"Face cache should map to offset only: {face_specs}")
|
||||
gated_face_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(9,), execution_ready={"face.offset"})
|
||||
_assert(gated_face_specs and all(spec.get("enabled") is True for spec in gated_face_specs), f"capability gate should enable verified face offset: {gated_face_specs}")
|
||||
_assert({"slot.position"} <= _capability_keys(cache, "slot:1"), f"slot.position should now be productized: {cache}")
|
||||
slot_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(31,), execution_ready={"slot.position"})
|
||||
_assert({"slot.width", "slot.depth", "slot.position"} <= _capability_keys(cache, "slot:1"), f"slot width/depth/position should now be productized: {cache}")
|
||||
slot_object = next((item for item in objects if isinstance(item, dict) and item.get("objectId") == "slot:slot:1"), None)
|
||||
_assert(isinstance(slot_object, dict), f"slot object should be normalized: {cache}")
|
||||
slot_signature = slot_object.get("geometrySignature")
|
||||
_assert(isinstance(slot_signature, dict) and slot_signature.get("depthFaceLocators"), f"slot depth locator should be preserved: {slot_signature}")
|
||||
_assert(isinstance(slot_signature, dict) and slot_signature.get("depthAxis") == [0.0, 0.0, -1.0], f"slot depth axis should be preserved: {slot_signature}")
|
||||
slot_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(31,), execution_ready={"slot.width", "slot.depth", "slot.position"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in slot_specs} == {"slot.position"},
|
||||
f"slot cache should expose only productized slot.position: {slot_specs}",
|
||||
{str(spec.get("scdm_capability_key")) for spec in slot_specs} == {"slot.width", "slot.depth", "slot.position"},
|
||||
f"slot cache should expose productized slot.width, slot.depth and slot.position: {slot_specs}",
|
||||
)
|
||||
_assert(slot_specs and slot_specs[0].get("enabled") is True, f"slot.position should enable when runner gate is ready: {slot_specs}")
|
||||
_assert(slot_specs and all(spec.get("enabled") is True for spec in slot_specs), f"slot width/depth/position should enable when runner gate is ready: {slot_specs}")
|
||||
slot_width_spec = next((spec for spec in slot_specs if spec.get("scdm_capability_key") == "slot.width"), None)
|
||||
_assert(slot_width_spec and slot_width_spec.get("value_type") == "positive", f"slot.width should use positive numeric input: {slot_specs}")
|
||||
slot_depth_spec = next((spec for spec in slot_specs if spec.get("scdm_capability_key") == "slot.depth"), None)
|
||||
_assert(slot_depth_spec and slot_depth_spec.get("value_type") == "positive", f"slot.depth should use positive numeric input: {slot_specs}")
|
||||
blocked_slot_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(31,), execution_ready={"face.offset"})
|
||||
_assert(blocked_slot_specs and all(spec.get("enabled") is False for spec in blocked_slot_specs), f"slot.position should honor runner gate: {blocked_slot_specs}")
|
||||
_assert({"boss.position"} <= _capability_keys(cache, "boss:1"), f"boss.position should now be productized: {cache}")
|
||||
boss_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(51,), execution_ready={"boss.position"})
|
||||
_assert(blocked_slot_specs and all(spec.get("enabled") is False for spec in blocked_slot_specs), f"slot capabilities should honor runner gate: {blocked_slot_specs}")
|
||||
_assert({"boss.diameter", "boss.height", "boss.position"} <= _capability_keys(cache, "boss:1"), f"boss diameter/height/position should now be productized: {cache}")
|
||||
boss_object = next((item for item in objects if isinstance(item, dict) and item.get("objectType") == "cylindrical_boss"), None)
|
||||
_assert(isinstance(boss_object, dict), f"boss object should be normalized: {cache}")
|
||||
boss_signature = boss_object.get("geometrySignature")
|
||||
_assert(isinstance(boss_signature, dict) and boss_signature.get("heightFaceLocators"), f"boss height locator should be preserved: {boss_signature}")
|
||||
_assert(isinstance(boss_signature, dict) and boss_signature.get("diameterFaceLocators"), f"boss diameter locator should be preserved: {boss_signature}")
|
||||
boss_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(51,), execution_ready={"boss.diameter", "boss.height", "boss.position"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in boss_specs} == {"boss.position"},
|
||||
f"boss cache should expose only productized boss.position: {boss_specs}",
|
||||
{str(spec.get("scdm_capability_key")) for spec in boss_specs} == {"boss.diameter", "boss.height", "boss.position"},
|
||||
f"boss cache should expose productized boss.diameter, boss.height and boss.position: {boss_specs}",
|
||||
)
|
||||
_assert(boss_specs and boss_specs[0].get("enabled") is True, f"boss.position should enable when runner gate is ready: {boss_specs}")
|
||||
_assert(boss_specs and all(spec.get("enabled") is True for spec in boss_specs), f"boss diameter/height/position should enable when runner gate is ready: {boss_specs}")
|
||||
blocked_boss_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(51,), execution_ready={"slot.position"})
|
||||
_assert(blocked_boss_specs and all(spec.get("enabled") is False for spec in blocked_boss_specs), f"boss.position should honor runner gate: {blocked_boss_specs}")
|
||||
_assert(blocked_boss_specs and all(spec.get("enabled") is False for spec in blocked_boss_specs), f"boss capabilities should honor runner gate: {blocked_boss_specs}")
|
||||
_assert({"round.radius", "feature.delete_round_or_chamfer"} <= _capability_keys(cache, "round:1"), f"round radius/delete should now be productized: {cache}")
|
||||
round_object = next((item for item in objects if isinstance(item, dict) and item.get("objectId") == "round:round:1"), None)
|
||||
_assert(isinstance(round_object, dict), f"round object should be normalized: {cache}")
|
||||
round_signature = round_object.get("geometrySignature")
|
||||
_assert(isinstance(round_signature, dict) and round_signature.get("isConstantRound") is True, f"round constant evidence should be preserved: {round_signature}")
|
||||
round_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(60,), execution_ready={"round.radius", "feature.delete_round_or_chamfer"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in round_specs} == {"round.radius", "feature.delete_round_or_chamfer"},
|
||||
f"round cache should expose productized radius and delete command: {round_specs}",
|
||||
)
|
||||
round_radius_spec = next((spec for spec in round_specs if spec.get("scdm_capability_key") == "round.radius"), None)
|
||||
round_delete_spec = next((spec for spec in round_specs if spec.get("scdm_capability_key") == "feature.delete_round_or_chamfer"), None)
|
||||
_assert(round_radius_spec and round_radius_spec.get("value_type") == "positive" and round_radius_spec.get("enabled") is True, f"round.radius should be executable when runner gate is ready: {round_specs}")
|
||||
_assert(round_delete_spec and round_delete_spec.get("value_type") == "command" and round_delete_spec.get("enabled") is True, f"round delete command should be executable when runner gate is ready: {round_specs}")
|
||||
_assert({"chamfer.distance"} <= _capability_keys(cache, "chamfer:1"), f"chamfer distance should now be productized: {cache}")
|
||||
chamfer_object = next((item for item in objects if isinstance(item, dict) and item.get("objectId") == "chamfer:chamfer:1"), None)
|
||||
_assert(isinstance(chamfer_object, dict), f"chamfer object should be normalized: {cache}")
|
||||
chamfer_signature = chamfer_object.get("geometrySignature")
|
||||
_assert(isinstance(chamfer_signature, dict) and chamfer_signature.get("isEqualDistanceChamfer") is True, f"chamfer equal-distance evidence should be preserved: {chamfer_signature}")
|
||||
chamfer_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(63,), execution_ready={"chamfer.distance"})
|
||||
_assert(
|
||||
{str(spec.get("scdm_capability_key")) for spec in chamfer_specs} == {"chamfer.distance"},
|
||||
f"chamfer cache should expose productized chamfer.distance: {chamfer_specs}",
|
||||
)
|
||||
_assert(chamfer_specs and chamfer_specs[0].get("value_type") == "positive" and chamfer_specs[0].get("enabled") is True, f"chamfer.distance should be executable when runner gate is ready: {chamfer_specs}")
|
||||
|
||||
raw_without_local_ids = {
|
||||
"schemaVersion": 1,
|
||||
@@ -452,6 +628,163 @@ def main() -> int:
|
||||
grouped_specs = property_specs_from_scdm_cache(enriched_group, selected_face_ids=(96,), execution_ready=True)
|
||||
grouped_keys = {str(spec.get("scdm_capability_key")) for spec in grouped_specs}
|
||||
_assert({"hole.diameter", "hole.position", "feature.fill"} <= grouped_keys, f"local Face IDs should attach to SCDM cylinder groups: {enriched_group}")
|
||||
body_pattern_without_face_ids = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "body:20",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 20.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 20, "faceOrdinals": [0], "globalFaceOrdinals": [300]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:21",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 25.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 21, "faceOrdinals": [0], "globalFaceOrdinals": [301]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:22",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 30.0, 0.0], "bboxSize": [1.0, 2.0, 3.0], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 22, "faceOrdinals": [0], "globalFaceOrdinals": [302]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
enriched_body_pattern = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(body_pattern_without_face_ids),
|
||||
[
|
||||
{"faceId": 300, "bodyIndex": 20, "faceOrdinal": 0, "globalFaceOrdinal": 300, "surfaceType": "plane"},
|
||||
{"faceId": 301, "bodyIndex": 21, "faceOrdinal": 0, "globalFaceOrdinal": 301, "surfaceType": "plane"},
|
||||
{"faceId": 302, "bodyIndex": 22, "faceOrdinal": 0, "globalFaceOrdinal": 302, "surfaceType": "plane"},
|
||||
],
|
||||
)
|
||||
body_pattern_specs_from_ordinals = property_specs_from_scdm_cache(enriched_body_pattern, selected_face_ids=(301,), execution_ready={"pattern.spacing"})
|
||||
_assert(
|
||||
any(spec.get("scdm_capability_key") == "pattern.spacing" for spec in body_pattern_specs_from_ordinals),
|
||||
f"body pattern spacing should attach local Face IDs from SCDM ordinals: {enriched_body_pattern}",
|
||||
)
|
||||
unit_scaled_face_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "m"},
|
||||
"diagnostics": {"availableCommands": [{"name": "OffsetFaces", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "body:0/face:9",
|
||||
"objectType": "face",
|
||||
"geometry": {"surfaceType": "plane", "axis": [-1.0, 0.0, 0.0], "planeOffset": -0.00125},
|
||||
"topologyHint": {"bodyIndex": 0, "faceOrdinal": 9, "globalFaceOrdinal": 9},
|
||||
"backendCommandCandidates": [{"operation": "pull_face_offset", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
unit_scaled_face_cache = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(unit_scaled_face_raw),
|
||||
[
|
||||
{
|
||||
"faceId": 9,
|
||||
"bodyIndex": 0,
|
||||
"faceOrdinal": 9,
|
||||
"globalFaceOrdinal": 9,
|
||||
"surfaceType": "plane",
|
||||
"axis": [-1.0, 0.0, 0.0],
|
||||
"planeOffset": -1.25,
|
||||
}
|
||||
],
|
||||
)
|
||||
unit_scaled_face_signature = unit_scaled_face_cache["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
_assert(abs(float(unit_scaled_face_signature.get("localUnitScale") or 0.0) - 0.001) <= 1.0e-12, f"ordinal-matched Face should infer SCDM/local unit scale: {unit_scaled_face_signature}")
|
||||
unit_scaled_face_specs = property_specs_from_scdm_cache(unit_scaled_face_cache, selected_face_ids=(9,), execution_ready=True)
|
||||
_assert(
|
||||
unit_scaled_face_specs and unit_scaled_face_specs[0].get("current_text") == "-1.25",
|
||||
f"SCDM length values should display in local model units: {unit_scaled_face_specs}",
|
||||
)
|
||||
unit_scaled_body_pattern_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "m"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "body:20",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 0.020, 0.0], "bboxSize": [0.001, 0.002, 0.003], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 20, "faceOrdinals": [0], "globalFaceOrdinals": [300]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:21",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 0.025, 0.0], "bboxSize": [0.001, 0.002, 0.003], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 21, "faceOrdinals": [0], "globalFaceOrdinals": [301]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
{
|
||||
"backendId": "body:22",
|
||||
"objectType": "body",
|
||||
"geometry": {"center": [0.0, 0.030, 0.0], "bboxSize": [0.001, 0.002, 0.003], "faceCount": 6, "edgeCount": 12},
|
||||
"topologyHint": {"bodyIndex": 22, "faceOrdinals": [0], "globalFaceOrdinals": [302]},
|
||||
"backendCommandCandidates": [{"operation": "move_body", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
},
|
||||
],
|
||||
}
|
||||
unit_scaled_body_pattern_cache = attach_local_face_ids_to_scdm_cache(
|
||||
map_scdm_raw_features(unit_scaled_body_pattern_raw),
|
||||
[
|
||||
{"faceId": 300, "bodyIndex": 20, "faceOrdinal": 0, "globalFaceOrdinal": 300, "surfaceType": "plane", "bboxMin": [-0.5, 19.0, 0.0], "bboxMax": [0.5, 21.0, 1.0]},
|
||||
{"faceId": 301, "bodyIndex": 21, "faceOrdinal": 0, "globalFaceOrdinal": 301, "surfaceType": "plane", "bboxMin": [-0.5, 24.0, 0.0], "bboxMax": [0.5, 26.0, 1.0]},
|
||||
{"faceId": 302, "bodyIndex": 22, "faceOrdinal": 0, "globalFaceOrdinal": 302, "surfaceType": "plane", "bboxMin": [-0.5, 29.0, 0.0], "bboxMax": [0.5, 31.0, 1.0]},
|
||||
{"faceId": 399, "bodyIndex": 99, "faceOrdinal": 0, "globalFaceOrdinal": 399, "surfaceType": "plane", "axis": [0.0, 0.0, 1.0], "planeOffset": 0.0, "bboxMin": [-1.0, 18.0, 0.0], "bboxMax": [1.0, 32.0, 0.0], "area": 28.0},
|
||||
],
|
||||
)
|
||||
unit_scaled_body_pattern = next(
|
||||
item for item in unit_scaled_body_pattern_cache["objects"] if item.get("objectType") == "linear_pattern" # type: ignore[index]
|
||||
)
|
||||
unit_scaled_body_signature = unit_scaled_body_pattern["geometrySignature"] # type: ignore[index]
|
||||
_assert(abs(float(unit_scaled_body_signature.get("localUnitScale") or 0.0) - 0.001) <= 1.0e-12, f"body pattern should infer unit scale from local body centers: {unit_scaled_body_signature}")
|
||||
_assert(unit_scaled_body_signature.get("supportFaceIds") == [399], f"body pattern should expose its support Face: {unit_scaled_body_signature}")
|
||||
fit = unit_scaled_body_signature.get("supportPatternFit")
|
||||
_assert(isinstance(fit, dict) and fit.get("supportFaceIds") == [399], f"body pattern should record support fit limits: {unit_scaled_body_signature}")
|
||||
_assert(abs(float(fit.get("maxSpacingLocal") or 0.0) - 6.0) <= 1.0e-9, f"support fit should limit centered spacing: {fit}")
|
||||
unit_scaled_body_specs = property_specs_from_scdm_cache(
|
||||
{"objects": [unit_scaled_body_pattern]},
|
||||
selected_face_ids=(399,),
|
||||
execution_ready={"pattern.spacing", "pattern.segment_spacing"},
|
||||
)
|
||||
_assert(
|
||||
unit_scaled_body_specs and unit_scaled_body_specs[0].get("current_text") == "5",
|
||||
f"support Face should expose pattern spacing in local units: {unit_scaled_body_specs}",
|
||||
)
|
||||
_assert(
|
||||
unit_scaled_body_specs[0].get("max_value") == 6.0,
|
||||
f"support Face pattern spacing should expose a local-unit hard limit: {unit_scaled_body_specs}",
|
||||
)
|
||||
local_segment_spec = next(
|
||||
(spec for spec in unit_scaled_body_specs if spec.get("scdm_capability_key") == "pattern.segment_spacing"),
|
||||
None,
|
||||
)
|
||||
_assert(
|
||||
isinstance(local_segment_spec, dict)
|
||||
and str(local_segment_spec.get("label") or "").startswith("Solid20-Solid21")
|
||||
and str(local_segment_spec.get("range_hint") or "").startswith("固定前项,移动后侧:")
|
||||
and "固定 Solid20" in str(local_segment_spec.get("range_hint") or "")
|
||||
and "平移 Solid21" in str(local_segment_spec.get("range_hint") or ""),
|
||||
f"local mapped pattern segment labels should use visible Solid IDs and explain motion semantics: {unit_scaled_body_specs}",
|
||||
)
|
||||
|
||||
missing_command_raw = {
|
||||
"schemaVersion": 1,
|
||||
@@ -479,6 +812,218 @@ def main() -> int:
|
||||
missing_command_specs = property_specs_from_scdm_cache(map_scdm_raw_features(missing_command_raw), selected_face_ids=(12,), execution_ready=True)
|
||||
_assert(missing_command_specs and missing_command_specs[0].get("enabled") is False, f"missing SCDM command should disable capability: {missing_command_specs}")
|
||||
_assert("OffsetFaces" in str(missing_command_specs[0].get("disabled_tip") or ""), f"disabled reason should name the missing SCDM command: {missing_command_specs}")
|
||||
missing_current_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "slot:missing-center",
|
||||
"objectType": "slot",
|
||||
"geometry": {"width": 2.0},
|
||||
"topologyHint": {"faceIds": [44]},
|
||||
"backendCommandCandidates": [{"operation": "move_slot", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
missing_current_specs = property_specs_from_scdm_cache(map_scdm_raw_features(missing_current_raw), selected_face_ids=(44,), execution_ready={"slot.width", "slot.position"})
|
||||
slot_position_missing_current = next((spec for spec in missing_current_specs if spec.get("scdm_capability_key") == "slot.position"), None)
|
||||
_assert(slot_position_missing_current and slot_position_missing_current.get("enabled") is False, f"missing current value should disable slot.position: {missing_current_specs}")
|
||||
_assert("没有返回可用于编辑的当前值" in str(slot_position_missing_current.get("disabled_tip") or ""), f"missing current value should be explained: {missing_current_specs}")
|
||||
slot_missing_depth_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "slot:missing-depth-locator",
|
||||
"objectType": "slot",
|
||||
"geometry": {"width": 2.0, "depth": 1.5, "depthAxis": [0.0, 0.0, -1.0], "center": [1.0, 2.0, 3.0]},
|
||||
"topologyHint": {"faceIds": [45], "bodyIndex": 0, "faceOrdinal": 45},
|
||||
"backendCommandCandidates": [{"operation": "change_slot_depth", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
slot_missing_depth_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(slot_missing_depth_locator_raw),
|
||||
selected_face_ids=(45,),
|
||||
execution_ready={"slot.depth"},
|
||||
)
|
||||
slot_depth_without_locator = next((spec for spec in slot_missing_depth_locator_specs if spec.get("scdm_capability_key") == "slot.depth"), None)
|
||||
_assert(slot_depth_without_locator and slot_depth_without_locator.get("enabled") is False, f"slot.depth should require a bottom face locator: {slot_missing_depth_locator_specs}")
|
||||
_assert("槽底面定位信息" in str(slot_depth_without_locator.get("disabled_tip") or ""), f"slot.depth missing locator should be explained: {slot_missing_depth_locator_specs}")
|
||||
slot_missing_depth_axis_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "slot:missing-depth-axis",
|
||||
"objectType": "slot",
|
||||
"geometry": {"width": 2.0, "depth": 1.5, "center": [1.0, 2.0, 3.0]},
|
||||
"topologyHint": {"faceIds": [46], "bodyIndex": 0, "faceOrdinal": 46, "depthFaceLocators": [{"bodyIndex": 0, "faceOrdinal": 46}]},
|
||||
"backendCommandCandidates": [{"operation": "change_slot_depth", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
slot_missing_depth_axis_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(slot_missing_depth_axis_raw),
|
||||
selected_face_ids=(46,),
|
||||
execution_ready={"slot.depth"},
|
||||
)
|
||||
slot_depth_without_axis = next((spec for spec in slot_missing_depth_axis_specs if spec.get("scdm_capability_key") == "slot.depth"), None)
|
||||
_assert(slot_depth_without_axis and slot_depth_without_axis.get("enabled") is False, f"slot.depth should require a depth axis: {slot_missing_depth_axis_specs}")
|
||||
_assert("槽深方向" in str(slot_depth_without_axis.get("disabled_tip") or ""), f"slot.depth missing axis should be explained: {slot_missing_depth_axis_specs}")
|
||||
boss_missing_height_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Move", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "boss:missing-height-locator",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometry": {"diameter": 3.0, "height": 4.0, "center": [0.0, 0.0, 2.0]},
|
||||
"topologyHint": {"faceIds": [55], "bodyIndex": 0, "faceOrdinal": 55},
|
||||
"backendCommandCandidates": [{"operation": "change_boss_height", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
boss_missing_height_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(boss_missing_height_locator_raw),
|
||||
selected_face_ids=(55,),
|
||||
execution_ready={"boss.height"},
|
||||
)
|
||||
boss_height_without_locator = next((spec for spec in boss_missing_height_locator_specs if spec.get("scdm_capability_key") == "boss.height"), None)
|
||||
_assert(boss_height_without_locator and boss_height_without_locator.get("enabled") is False, f"boss.height should require a top face locator: {boss_missing_height_locator_specs}")
|
||||
_assert("顶面定位信息" in str(boss_height_without_locator.get("disabled_tip") or ""), f"boss.height missing locator should be explained: {boss_missing_height_locator_specs}")
|
||||
boss_missing_diameter_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "OffsetFaces", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "boss:missing-diameter-locator",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometry": {"diameter": 3.0, "height": 4.0, "center": [0.0, 0.0, 2.0]},
|
||||
"topologyHint": {"faceIds": [56], "bodyIndex": 0, "faceOrdinal": 56},
|
||||
"backendCommandCandidates": [{"operation": "change_boss_diameter", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
boss_missing_diameter_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(boss_missing_diameter_locator_raw),
|
||||
selected_face_ids=(56,),
|
||||
execution_ready={"boss.diameter"},
|
||||
)
|
||||
boss_diameter_without_locator = next((spec for spec in boss_missing_diameter_locator_specs if spec.get("scdm_capability_key") == "boss.diameter"), None)
|
||||
_assert(boss_diameter_without_locator and boss_diameter_without_locator.get("enabled") is False, f"boss.diameter should require a side face locator: {boss_missing_diameter_locator_specs}")
|
||||
_assert("侧壁定位信息" in str(boss_diameter_without_locator.get("disabled_tip") or ""), f"boss.diameter missing locator should be explained: {boss_missing_diameter_locator_specs}")
|
||||
round_missing_constant_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "ConstantRound", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "round:missing-constant",
|
||||
"objectType": "round",
|
||||
"geometry": {"roundInfo": {"radius": 1.0, "isRound": True}},
|
||||
"topologyHint": {"faceIds": [61], "bodyIndex": 0, "faceOrdinal": 61},
|
||||
"backendCommandCandidates": [{"operation": "change_round_radius", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
round_missing_constant_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(round_missing_constant_raw),
|
||||
selected_face_ids=(61,),
|
||||
execution_ready={"round.radius"},
|
||||
)
|
||||
round_without_constant = next((spec for spec in round_missing_constant_specs if spec.get("scdm_capability_key") == "round.radius"), None)
|
||||
_assert(round_without_constant and round_without_constant.get("enabled") is False, f"round.radius should require constant-round evidence: {round_missing_constant_specs}")
|
||||
_assert("等半径圆角证据" in str(round_without_constant.get("disabled_tip") or ""), f"round.radius missing constant evidence should be explained: {round_missing_constant_specs}")
|
||||
round_missing_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "ConstantRound", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "round:missing-locator",
|
||||
"objectType": "round",
|
||||
"geometry": {"roundInfo": {"radius": 1.0, "isConstant": True, "isRound": True}},
|
||||
"topologyHint": {"faceIds": [62]},
|
||||
"backendCommandCandidates": [{"operation": "change_round_radius", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
round_missing_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(round_missing_locator_raw),
|
||||
selected_face_ids=(62,),
|
||||
execution_ready={"round.radius"},
|
||||
)
|
||||
round_without_locator = next((spec for spec in round_missing_locator_specs if spec.get("scdm_capability_key") == "round.radius"), None)
|
||||
_assert(round_without_locator and round_without_locator.get("enabled") is False, f"round.radius should require SCDM face locator evidence: {round_missing_locator_specs}")
|
||||
_assert("可定位的圆角面" in str(round_without_locator.get("disabled_tip") or ""), f"round.radius missing locator should be explained: {round_missing_locator_specs}")
|
||||
chamfer_missing_equal_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Chamfer", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "chamfer:missing-equal",
|
||||
"objectType": "chamfer",
|
||||
"geometry": {"chamferInfo": {"distance": 0.8, "isChamfer": True}},
|
||||
"topologyHint": {"faceIds": [64], "bodyIndex": 0, "faceOrdinal": 64},
|
||||
"backendCommandCandidates": [{"operation": "change_chamfer_distance", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
chamfer_missing_equal_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(chamfer_missing_equal_raw),
|
||||
selected_face_ids=(64,),
|
||||
execution_ready={"chamfer.distance"},
|
||||
)
|
||||
chamfer_without_equal = next((spec for spec in chamfer_missing_equal_specs if spec.get("scdm_capability_key") == "chamfer.distance"), None)
|
||||
_assert(chamfer_without_equal and chamfer_without_equal.get("enabled") is False, f"chamfer.distance should require equal-distance evidence: {chamfer_missing_equal_specs}")
|
||||
_assert("等距倒角证据" in str(chamfer_without_equal.get("disabled_tip") or ""), f"chamfer.distance missing equal evidence should be explained: {chamfer_missing_equal_specs}")
|
||||
chamfer_missing_locator_raw = {
|
||||
"schemaVersion": 1,
|
||||
"backend": {"name": "SCDM", "version": "v222"},
|
||||
"model": {"path": str(step_path), "fingerprint": "abc123", "unit": "mm"},
|
||||
"diagnostics": {"availableCommands": [{"name": "Chamfer", "available": True}]},
|
||||
"objects": [
|
||||
{
|
||||
"backendId": "chamfer:missing-locator",
|
||||
"objectType": "chamfer",
|
||||
"geometry": {"chamferInfo": {"distance": 0.8, "isEqualDistance": True, "isChamfer": True}},
|
||||
"topologyHint": {"faceIds": [65]},
|
||||
"backendCommandCandidates": [{"operation": "change_chamfer_distance", "enabled": True}],
|
||||
"rawLimitations": [],
|
||||
}
|
||||
],
|
||||
}
|
||||
chamfer_missing_locator_specs = property_specs_from_scdm_cache(
|
||||
map_scdm_raw_features(chamfer_missing_locator_raw),
|
||||
selected_face_ids=(65,),
|
||||
execution_ready={"chamfer.distance"},
|
||||
)
|
||||
chamfer_without_locator = next((spec for spec in chamfer_missing_locator_specs if spec.get("scdm_capability_key") == "chamfer.distance"), None)
|
||||
_assert(chamfer_without_locator and chamfer_without_locator.get("enabled") is False, f"chamfer.distance should require SCDM face locator evidence: {chamfer_missing_locator_specs}")
|
||||
_assert("可定位的倒角面" in str(chamfer_without_locator.get("disabled_tip") or ""), f"chamfer.distance missing locator should be explained: {chamfer_missing_locator_specs}")
|
||||
diagnostics = cache.get("diagnostics")
|
||||
_assert(isinstance(diagnostics, dict), f"cache diagnostics should be present: {cache}")
|
||||
raw_summary = diagnostics.get("raw_summary")
|
||||
@@ -499,11 +1044,18 @@ def main() -> int:
|
||||
and feature_inventory.get("operationCounts", {}).get("change_slot_width") == 1,
|
||||
f"SCDM feature inventory should be preserved: {cache}",
|
||||
)
|
||||
component_instances = diagnostics.get("component_instances")
|
||||
_assert(
|
||||
isinstance(component_instances, list)
|
||||
and component_instances
|
||||
and component_instances[0].get("componentPath") == [0],
|
||||
f"SCDM component occurrence diagnostics should be preserved: {cache}",
|
||||
)
|
||||
geometry_hints = diagnostics.get("geometry_candidate_hints")
|
||||
_assert(isinstance(geometry_hints, list) and geometry_hints, f"SCDM structural candidate hints should be produced: {cache}")
|
||||
hint_keys = {str(item.get("capabilityKey")) for item in geometry_hints if isinstance(item, dict)}
|
||||
_assert(
|
||||
{"slot.width", "boss.height", "round.radius", "chamfer.distance", "pattern.spacing", "shell.thickness"} <= hint_keys,
|
||||
{"pattern.instance_position", "shell.thickness"} <= hint_keys and "slot.depth" not in hint_keys and "pattern.spacing" not in hint_keys,
|
||||
f"S7 geometry hints should cover planned feature families: {hint_keys}",
|
||||
)
|
||||
derived_candidates = diagnostics.get("derived_feature_candidates")
|
||||
@@ -516,20 +1068,103 @@ def main() -> int:
|
||||
_assert(pattern_signature.get("instanceCount") == 3, f"linear pattern count should be preserved: {pattern_signature}")
|
||||
_assert(pattern_signature.get("axis") == [1.0, 0.0, 0.0], f"linear pattern axis should be preserved: {pattern_signature}")
|
||||
_assert(len(pattern_signature.get("instanceCenters") or []) == 3, f"linear pattern centers should be preserved: {pattern_signature}")
|
||||
_assert(len(pattern_signature.get("patternInstances") or []) == 3, f"linear pattern instance locators should be preserved: {pattern_signature}")
|
||||
_assert({"pattern.spacing"} <= _capability_keys(cache, "derived:linear_pattern"), f"derived linear pattern should expose productized spacing: {cache}")
|
||||
thin_wall_candidate = next((item for item in derived_candidates if isinstance(item, dict) and item.get("objectType") == "thin_wall"), None)
|
||||
_assert(isinstance(thin_wall_candidate, dict), f"paired planar faces should produce a thin-wall candidate: {derived_candidates}")
|
||||
thin_wall_signature = thin_wall_candidate.get("geometrySignature")
|
||||
_assert(isinstance(thin_wall_signature, dict), f"thin-wall candidate should keep a geometry signature: {thin_wall_candidate}")
|
||||
_assert(thin_wall_signature.get("thickness") == 1.2, f"thin-wall thickness should be preserved: {thin_wall_signature}")
|
||||
_assert(thin_wall_signature.get("thicknessAxis") == [0.0, 0.0, 1.0], f"thin-wall axis should be preserved: {thin_wall_signature}")
|
||||
_assert(len(thin_wall_signature.get("wallFaceLocators") or []) == 2, f"thin-wall face locators should be preserved: {thin_wall_signature}")
|
||||
body_pattern_candidate = next(
|
||||
(
|
||||
item
|
||||
for item in derived_candidates
|
||||
if isinstance(item, dict)
|
||||
and item.get("objectType") == "linear_pattern"
|
||||
and isinstance(item.get("geometrySignature"), dict)
|
||||
and item.get("geometrySignature", {}).get("patternKind") == "body"
|
||||
),
|
||||
None,
|
||||
)
|
||||
_assert(isinstance(body_pattern_candidate, dict), f"repeated bodies should produce a body linear-pattern candidate: {derived_candidates}")
|
||||
body_pattern_signature = body_pattern_candidate.get("geometrySignature")
|
||||
_assert(isinstance(body_pattern_signature, dict), f"body pattern should keep a geometry signature: {body_pattern_candidate}")
|
||||
_assert(body_pattern_signature.get("spacing") == 5.0, f"body pattern spacing should be preserved: {body_pattern_signature}")
|
||||
_assert(body_pattern_signature.get("axis") == [0.0, 1.0, 0.0], f"body pattern axis should be preserved: {body_pattern_signature}")
|
||||
_assert(body_pattern_signature.get("bodyIndices") == [20, 21, 22], f"body pattern body indices should be preserved: {body_pattern_signature}")
|
||||
body_instances = body_pattern_signature.get("patternInstances") or []
|
||||
_assert(len(body_instances) == 3 and all(item.get("instanceKind") == "body" for item in body_instances), f"body pattern instances should stay body-level: {body_pattern_signature}")
|
||||
_assert(all(item.get("componentLocators") for item in body_instances if isinstance(item, dict)), f"body pattern instances should preserve component occurrence locators: {body_pattern_signature}")
|
||||
body_pattern_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(301,), execution_ready={"pattern.spacing", "pattern.segment_spacing"})
|
||||
body_pattern_spacing = [
|
||||
spec for spec in body_pattern_specs if spec.get("scdm_capability_key") == "pattern.spacing"
|
||||
]
|
||||
_assert(body_pattern_spacing, f"selecting a body member face should keep body pattern spacing as a recognized capability: {body_pattern_specs}")
|
||||
_assert(
|
||||
body_pattern_spacing[0].get("enabled") is True,
|
||||
f"body pattern spacing should be executable when every member has a component occurrence locator: {body_pattern_spacing}",
|
||||
)
|
||||
body_segment_specs = [
|
||||
spec for spec in body_pattern_specs if spec.get("scdm_capability_key") == "pattern.segment_spacing"
|
||||
]
|
||||
_assert(len(body_segment_specs) == 2, f"three body pattern members should expose two local spacing segments: {body_pattern_specs}")
|
||||
_assert(
|
||||
"零件" in str(body_segment_specs[0].get("label") or "")
|
||||
and body_segment_specs[0].get("enabled") is True
|
||||
and body_segment_specs[0].get("scdm_backend_operation") == "change_pattern_segment_spacing",
|
||||
f"local body pattern spacing should be executable and name the adjacent members: {body_segment_specs}",
|
||||
)
|
||||
body_segment_modes = body_segment_specs[0].get("scope_modes")
|
||||
_assert(
|
||||
isinstance(body_segment_modes, dict)
|
||||
and {"fix_left_move_right", "fix_right_move_left", "split_keep_center", "move_single_right"} <= set(body_segment_modes),
|
||||
f"local pattern spacing should expose several explicit modeling intents: {body_segment_specs}",
|
||||
)
|
||||
_assert(
|
||||
body_segment_modes["fix_left_move_right"].get("enabled") is True
|
||||
and body_segment_modes["fix_right_move_left"].get("enabled") is True
|
||||
and body_segment_modes["split_keep_center"].get("enabled") is True
|
||||
and body_segment_modes["move_single_right"].get("enabled") is False,
|
||||
f"implemented and blocked local spacing intents should be explicit: {body_segment_modes}",
|
||||
)
|
||||
for mode in body_segment_modes.values():
|
||||
if not isinstance(mode, dict):
|
||||
continue
|
||||
label = str(mode.get("label") or "").strip()
|
||||
tip = str(mode.get("enabled_tip") or mode.get("disabled_tip") or mode.get("range_hint") or "").strip()
|
||||
_assert(label and tip.startswith(label), f"modeling-intent tooltip should start with its intent label: {body_segment_modes}")
|
||||
_assert(
|
||||
"固定前项" in str(body_segment_specs[0].get("scope_text") or "")
|
||||
and "移动后侧" in str(body_segment_specs[0].get("scope_text") or ""),
|
||||
f"local pattern spacing should expose motion semantics in the intent column: {body_segment_specs}",
|
||||
)
|
||||
body_segment_signature = body_segment_specs[0].get("scdm_geometry_signature")
|
||||
_assert(
|
||||
isinstance(body_segment_signature, dict)
|
||||
and body_segment_signature.get("segmentIndex") == 0
|
||||
and body_segment_signature.get("movingSide") == "after",
|
||||
f"local segment specs should carry segment execution metadata: {body_segment_specs}",
|
||||
)
|
||||
pattern_specs = property_specs_from_scdm_cache(cache, selected_face_ids=(201,), execution_ready={"pattern.spacing", "pattern.segment_spacing"})
|
||||
pattern_spacing_spec = next((spec for spec in pattern_specs if spec.get("scdm_capability_key") == "pattern.spacing"), None)
|
||||
_assert(pattern_spacing_spec and pattern_spacing_spec.get("enabled") is True, f"pattern.spacing should enable for selected pattern member: {pattern_specs}")
|
||||
_assert(pattern_spacing_spec.get("value_type") == "positive", f"pattern.spacing should use positive numeric input: {pattern_specs}")
|
||||
face_segment_specs = [
|
||||
spec for spec in pattern_specs if spec.get("scdm_capability_key") == "pattern.segment_spacing"
|
||||
]
|
||||
_assert(len(face_segment_specs) == 2, f"three face pattern members should expose two local spacing segments: {pattern_specs}")
|
||||
_assert(
|
||||
str(face_segment_specs[0].get("label") or "").startswith("Face"),
|
||||
f"face pattern local spacing should name the adjacent Face members instead of raw ordinal numbers: {face_segment_specs}",
|
||||
)
|
||||
not_productized = diagnostics.get("discovered_not_productized")
|
||||
_assert(isinstance(not_productized, list) and not_productized, f"round candidate should stay diagnostic only: {cache}")
|
||||
_assert(isinstance(not_productized, list) and not_productized, f"non-productized candidates should stay diagnostic only: {cache}")
|
||||
planned = diagnostics.get("planned_not_productized")
|
||||
_assert(isinstance(planned, list), f"planned diagnostics should be present: {cache}")
|
||||
planned_keys = {str(item.get("capabilityKey")) for item in planned if isinstance(item, dict)}
|
||||
expected_planned = {
|
||||
"slot.width",
|
||||
"slot.depth",
|
||||
"boss.height",
|
||||
"boss.diameter",
|
||||
"round.radius",
|
||||
"feature.delete_round_or_chamfer",
|
||||
"chamfer.distance",
|
||||
"pattern.spacing",
|
||||
"pattern.instance_position",
|
||||
"shell.thickness",
|
||||
}
|
||||
|
||||
@@ -44,6 +44,29 @@ def _hole(object_id: str, face_id: int, *, diameter: float, center: tuple[float,
|
||||
}
|
||||
|
||||
|
||||
def _feature(
|
||||
object_id: str,
|
||||
object_type: str,
|
||||
face_id: int,
|
||||
*,
|
||||
center: tuple[float, float, float],
|
||||
capability_key: str,
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"objectId": object_id,
|
||||
"objectType": object_type,
|
||||
"geometrySignature": {
|
||||
"objectType": object_type,
|
||||
"faceIds": [face_id],
|
||||
"center": list(center),
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
},
|
||||
"capabilities": [
|
||||
{"key": capability_key, "currentValue": list(center)},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _cache(*objects: dict[str, object]) -> dict[str, object]:
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
@@ -128,6 +151,419 @@ def main() -> int:
|
||||
position_check = check_scdm_target(position_after["objects"][0], capability_key="hole.position", expected_target=[2.0, 1.0, 6.0])
|
||||
_assert(position_check.get("ok") is True, f"position target should pass: {position_check}")
|
||||
|
||||
before_slot = _cache(_feature("slot:30", "slot", 30, center=(1.0, 2.0, 3.0), capability_key="slot.position"))
|
||||
after_slot = _cache(_feature("slot:40", "slot", 40, center=(1.0, 2.0, 6.0), capability_key="slot.position"))
|
||||
slot_before_signature = before_slot["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
slot_match = match_scdm_object_by_signature(slot_before_signature, after_slot, capability_key="slot.position")
|
||||
_assert(slot_match.get("status") == "unique", f"moved slot should match without old center lock: {slot_match}")
|
||||
slot_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=slot_before_signature,
|
||||
before_cache=before_slot,
|
||||
after_cache=after_slot,
|
||||
capability_key="slot.position",
|
||||
expected_target=[1.0, 2.0, 6.0],
|
||||
edited_object_id="slot:30",
|
||||
)
|
||||
_assert(slot_ok.get("ok") is True, f"slot.position result should validate target center: {slot_ok}")
|
||||
_assert(slot_ok.get("targetCheck", {}).get("ok") is True, f"slot target should be checked: {slot_ok}")
|
||||
before_slot_width = _cache(
|
||||
{
|
||||
"objectId": "slot:30",
|
||||
"objectType": "slot",
|
||||
"geometrySignature": {
|
||||
"objectType": "slot",
|
||||
"faceIds": [30],
|
||||
"center": [1.0, 2.0, 3.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"width": 2.0,
|
||||
},
|
||||
"capabilities": [{"key": "slot.width", "currentValue": 2.0}],
|
||||
}
|
||||
)
|
||||
after_slot_width = _cache(
|
||||
{
|
||||
"objectId": "slot:40",
|
||||
"objectType": "slot",
|
||||
"geometrySignature": {
|
||||
"objectType": "slot",
|
||||
"faceIds": [40],
|
||||
"center": [1.0, 2.0, 3.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"width": 2.5,
|
||||
},
|
||||
"capabilities": [{"key": "slot.width", "currentValue": 2.5}],
|
||||
}
|
||||
)
|
||||
slot_width_before_signature = before_slot_width["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
slot_width_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=slot_width_before_signature,
|
||||
before_cache=before_slot_width,
|
||||
after_cache=after_slot_width,
|
||||
capability_key="slot.width",
|
||||
expected_target=2.5,
|
||||
edited_object_id="slot:30",
|
||||
)
|
||||
_assert(slot_width_ok.get("ok") is True, f"slot.width result should validate target width: {slot_width_ok}")
|
||||
slot_width_mismatch = check_scdm_target(after_slot_width["objects"][0], capability_key="slot.width", expected_target=2.1) # type: ignore[index]
|
||||
_assert(slot_width_mismatch.get("ok") is False and slot_width_mismatch.get("reason") == "target-mismatch", f"slot.width mismatch should fail: {slot_width_mismatch}")
|
||||
before_slot_depth = _cache(
|
||||
{
|
||||
"objectId": "slot:31",
|
||||
"objectType": "slot",
|
||||
"geometrySignature": {
|
||||
"objectType": "slot",
|
||||
"faceIds": [31],
|
||||
"center": [1.0, 2.0, 3.0],
|
||||
"depth": 1.5,
|
||||
"depthAxis": [0.0, 0.0, -1.0],
|
||||
},
|
||||
"capabilities": [{"key": "slot.depth", "currentValue": 1.5}],
|
||||
}
|
||||
)
|
||||
after_slot_depth = _cache(
|
||||
{
|
||||
"objectId": "slot:41",
|
||||
"objectType": "slot",
|
||||
"geometrySignature": {
|
||||
"objectType": "slot",
|
||||
"faceIds": [41],
|
||||
"center": [1.0, 2.0, 3.0],
|
||||
"depth": 2.0,
|
||||
"depthAxis": [0.0, 0.0, -1.0],
|
||||
},
|
||||
"capabilities": [{"key": "slot.depth", "currentValue": 2.0}],
|
||||
}
|
||||
)
|
||||
slot_depth_before_signature = before_slot_depth["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
slot_depth_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=slot_depth_before_signature,
|
||||
before_cache=before_slot_depth,
|
||||
after_cache=after_slot_depth,
|
||||
capability_key="slot.depth",
|
||||
expected_target=2.0,
|
||||
edited_object_id="slot:31",
|
||||
)
|
||||
_assert(slot_depth_ok.get("ok") is True, f"slot.depth result should validate target depth: {slot_depth_ok}")
|
||||
slot_depth_mismatch = check_scdm_target(after_slot_depth["objects"][0], capability_key="slot.depth", expected_target=1.5) # type: ignore[index]
|
||||
_assert(slot_depth_mismatch.get("ok") is False and slot_depth_mismatch.get("reason") == "target-mismatch", f"slot.depth mismatch should fail: {slot_depth_mismatch}")
|
||||
|
||||
boss_after = _feature("boss:50", "cylindrical_boss", 50, center=(3.0, 0.0, 2.0), capability_key="boss.position")
|
||||
boss_check = check_scdm_target(boss_after, capability_key="boss.position", expected_target=[3.0, 0.0, 2.0])
|
||||
_assert(boss_check.get("ok") is True, f"boss.position target should pass: {boss_check}")
|
||||
boss_mismatch = check_scdm_target(boss_after, capability_key="boss.position", expected_target=[4.0, 0.0, 2.0])
|
||||
_assert(boss_mismatch.get("ok") is False and boss_mismatch.get("reason") == "target-mismatch", f"boss.position mismatch should fail: {boss_mismatch}")
|
||||
before_boss_height = _cache(
|
||||
{
|
||||
"objectId": "boss:50",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometrySignature": {
|
||||
"objectType": "cylindrical_boss",
|
||||
"faceIds": [50, 51, 52],
|
||||
"center": [0.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"diameter": 3.0,
|
||||
"height": 4.0,
|
||||
},
|
||||
"capabilities": [{"key": "boss.height", "currentValue": 4.0}],
|
||||
}
|
||||
)
|
||||
after_boss_height = _cache(
|
||||
{
|
||||
"objectId": "boss:55",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometrySignature": {
|
||||
"objectType": "cylindrical_boss",
|
||||
"faceIds": [55, 56, 57],
|
||||
"center": [0.0, 0.0, 2.75],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"diameter": 3.0,
|
||||
"height": 5.5,
|
||||
},
|
||||
"capabilities": [{"key": "boss.height", "currentValue": 5.5}],
|
||||
}
|
||||
)
|
||||
boss_height_signature = before_boss_height["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
boss_height_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=boss_height_signature,
|
||||
before_cache=before_boss_height,
|
||||
after_cache=after_boss_height,
|
||||
capability_key="boss.height",
|
||||
expected_target=5.5,
|
||||
edited_object_id="boss:50",
|
||||
)
|
||||
_assert(boss_height_ok.get("ok") is True, f"boss.height result should validate target height: {boss_height_ok}")
|
||||
boss_height_mismatch = check_scdm_target(after_boss_height["objects"][0], capability_key="boss.height", expected_target=4.5) # type: ignore[index]
|
||||
_assert(boss_height_mismatch.get("ok") is False and boss_height_mismatch.get("reason") == "target-mismatch", f"boss.height mismatch should fail: {boss_height_mismatch}")
|
||||
before_boss_diameter = _cache(
|
||||
{
|
||||
"objectId": "boss:60",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometrySignature": {
|
||||
"objectType": "cylindrical_boss",
|
||||
"faceIds": [60, 61, 62],
|
||||
"center": [0.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"diameter": 3.0,
|
||||
"height": 4.0,
|
||||
},
|
||||
"capabilities": [{"key": "boss.diameter", "currentValue": 3.0}],
|
||||
}
|
||||
)
|
||||
after_boss_diameter = _cache(
|
||||
{
|
||||
"objectId": "boss:63",
|
||||
"objectType": "cylindrical_boss",
|
||||
"geometrySignature": {
|
||||
"objectType": "cylindrical_boss",
|
||||
"faceIds": [63, 64, 65],
|
||||
"center": [0.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"diameter": 4.5,
|
||||
"height": 4.0,
|
||||
},
|
||||
"capabilities": [{"key": "boss.diameter", "currentValue": 4.5}],
|
||||
}
|
||||
)
|
||||
boss_diameter_signature = before_boss_diameter["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
boss_diameter_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=boss_diameter_signature,
|
||||
before_cache=before_boss_diameter,
|
||||
after_cache=after_boss_diameter,
|
||||
capability_key="boss.diameter",
|
||||
expected_target=4.5,
|
||||
edited_object_id="boss:60",
|
||||
)
|
||||
_assert(boss_diameter_ok.get("ok") is True, f"boss.diameter result should validate target diameter: {boss_diameter_ok}")
|
||||
boss_diameter_mismatch = check_scdm_target(after_boss_diameter["objects"][0], capability_key="boss.diameter", expected_target=3.5) # type: ignore[index]
|
||||
_assert(boss_diameter_mismatch.get("ok") is False and boss_diameter_mismatch.get("reason") == "target-mismatch", f"boss.diameter mismatch should fail: {boss_diameter_mismatch}")
|
||||
|
||||
before_round_radius = _cache(
|
||||
{
|
||||
"objectId": "round:70",
|
||||
"objectType": "round",
|
||||
"geometrySignature": {
|
||||
"objectType": "round",
|
||||
"faceIds": [70],
|
||||
"center": [1.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"radius": 0.5,
|
||||
"isConstantRound": True,
|
||||
},
|
||||
"capabilities": [{"key": "round.radius", "currentValue": 0.5}],
|
||||
}
|
||||
)
|
||||
after_round_radius = _cache(
|
||||
{
|
||||
"objectId": "round:71",
|
||||
"objectType": "round",
|
||||
"geometrySignature": {
|
||||
"objectType": "round",
|
||||
"faceIds": [71],
|
||||
"center": [1.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"radius": 0.75,
|
||||
"isConstantRound": True,
|
||||
},
|
||||
"capabilities": [{"key": "round.radius", "currentValue": 0.75}],
|
||||
}
|
||||
)
|
||||
round_radius_signature = before_round_radius["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
round_radius_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=round_radius_signature,
|
||||
before_cache=before_round_radius,
|
||||
after_cache=after_round_radius,
|
||||
capability_key="round.radius",
|
||||
expected_target=0.75,
|
||||
edited_object_id="round:70",
|
||||
)
|
||||
_assert(round_radius_ok.get("ok") is True, f"round.radius result should validate target radius: {round_radius_ok}")
|
||||
round_radius_mismatch = check_scdm_target(after_round_radius["objects"][0], capability_key="round.radius", expected_target=0.5) # type: ignore[index]
|
||||
_assert(round_radius_mismatch.get("ok") is False and round_radius_mismatch.get("reason") == "target-mismatch", f"round.radius mismatch should fail: {round_radius_mismatch}")
|
||||
|
||||
before_chamfer_distance = _cache(
|
||||
{
|
||||
"objectId": "chamfer:80",
|
||||
"objectType": "chamfer",
|
||||
"geometrySignature": {
|
||||
"objectType": "chamfer",
|
||||
"faceIds": [80],
|
||||
"center": [2.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"distance": 0.8,
|
||||
"isEqualDistanceChamfer": True,
|
||||
},
|
||||
"capabilities": [{"key": "chamfer.distance", "currentValue": 0.8}],
|
||||
}
|
||||
)
|
||||
after_chamfer_distance = _cache(
|
||||
{
|
||||
"objectId": "chamfer:81",
|
||||
"objectType": "chamfer",
|
||||
"geometrySignature": {
|
||||
"objectType": "chamfer",
|
||||
"faceIds": [81],
|
||||
"center": [2.0, 0.0, 2.0],
|
||||
"axis": [0.0, 0.0, 1.0],
|
||||
"distance": 1.2,
|
||||
"isEqualDistanceChamfer": True,
|
||||
},
|
||||
"capabilities": [{"key": "chamfer.distance", "currentValue": 1.2}],
|
||||
}
|
||||
)
|
||||
chamfer_distance_signature = before_chamfer_distance["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
chamfer_distance_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=chamfer_distance_signature,
|
||||
before_cache=before_chamfer_distance,
|
||||
after_cache=after_chamfer_distance,
|
||||
capability_key="chamfer.distance",
|
||||
expected_target=1.2,
|
||||
edited_object_id="chamfer:80",
|
||||
)
|
||||
_assert(chamfer_distance_ok.get("ok") is True, f"chamfer.distance result should validate target distance: {chamfer_distance_ok}")
|
||||
chamfer_distance_mismatch = check_scdm_target(after_chamfer_distance["objects"][0], capability_key="chamfer.distance", expected_target=0.8) # type: ignore[index]
|
||||
_assert(chamfer_distance_mismatch.get("ok") is False and chamfer_distance_mismatch.get("reason") == "target-mismatch", f"chamfer.distance mismatch should fail: {chamfer_distance_mismatch}")
|
||||
|
||||
before_pattern_spacing = _cache(
|
||||
{
|
||||
"objectId": "pattern:holes",
|
||||
"objectType": "linear_pattern",
|
||||
"geometrySignature": {
|
||||
"objectType": "linear_pattern",
|
||||
"faceIds": [85, 87, 89],
|
||||
"center": [5.0, 0.0, 0.0],
|
||||
"axis": [1.0, 0.0, 0.0],
|
||||
"spacing": 5.0,
|
||||
"pitch": 5.0,
|
||||
"instanceCount": 3,
|
||||
"instanceCenters": [[0.0, 0.0, 0.0], [5.0, 0.0, 0.0], [10.0, 0.0, 0.0]],
|
||||
},
|
||||
"capabilities": [{"key": "pattern.spacing", "currentValue": 5.0}],
|
||||
}
|
||||
)
|
||||
after_pattern_spacing = _cache(
|
||||
{
|
||||
"objectId": "pattern:holes-new",
|
||||
"objectType": "linear_pattern",
|
||||
"geometrySignature": {
|
||||
"objectType": "linear_pattern",
|
||||
"faceIds": [90, 91, 92],
|
||||
"center": [7.5, 0.0, 0.0],
|
||||
"axis": [1.0, 0.0, 0.0],
|
||||
"spacing": 7.5,
|
||||
"pitch": 7.5,
|
||||
"instanceCount": 3,
|
||||
"instanceCenters": [[0.0, 0.0, 0.0], [7.5, 0.0, 0.0], [15.0, 0.0, 0.0]],
|
||||
},
|
||||
"capabilities": [{"key": "pattern.spacing", "currentValue": 7.5}],
|
||||
}
|
||||
)
|
||||
pattern_spacing_before_signature = before_pattern_spacing["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
pattern_spacing_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=pattern_spacing_before_signature,
|
||||
before_cache=before_pattern_spacing,
|
||||
after_cache=after_pattern_spacing,
|
||||
capability_key="pattern.spacing",
|
||||
expected_target=7.5,
|
||||
edited_object_id="pattern:holes",
|
||||
)
|
||||
_assert(pattern_spacing_ok.get("ok") is True, f"pattern.spacing result should validate target spacing: {pattern_spacing_ok}")
|
||||
pattern_spacing_mismatch = check_scdm_target(after_pattern_spacing["objects"][0], capability_key="pattern.spacing", expected_target=5.0) # type: ignore[index]
|
||||
_assert(pattern_spacing_mismatch.get("ok") is False and pattern_spacing_mismatch.get("reason") == "target-mismatch", f"pattern.spacing mismatch should fail: {pattern_spacing_mismatch}")
|
||||
before_pattern_segment = _cache(
|
||||
{
|
||||
"objectId": "pattern:holes",
|
||||
"objectType": "linear_pattern",
|
||||
"geometrySignature": {
|
||||
"objectType": "linear_pattern",
|
||||
"faceIds": [85, 87, 89],
|
||||
"center": [5.0, 0.0, 0.0],
|
||||
"axis": [1.0, 0.0, 0.0],
|
||||
"spacing": 5.0,
|
||||
"pitch": 5.0,
|
||||
"segmentIndex": 1,
|
||||
"movingSide": "after",
|
||||
"patternInstances": [
|
||||
{"sourceObjectId": "a", "center": [0.0, 0.0, 0.0], "faceIds": [85]},
|
||||
{"sourceObjectId": "b", "center": [5.0, 0.0, 0.0], "faceIds": [87]},
|
||||
{"sourceObjectId": "c", "center": [10.0, 0.0, 0.0], "faceIds": [89]},
|
||||
],
|
||||
},
|
||||
"capabilities": [{"key": "pattern.segment_spacing", "currentValue": 5.0}],
|
||||
},
|
||||
_hole("hole:unrelated", 999, diameter=1.0, center=(100.0, 0.0, 0.0)),
|
||||
)
|
||||
after_pattern_segment = _cache(_hole("hole:unrelated-new", 999, diameter=1.0, center=(100.0, 0.0, 0.0)))
|
||||
pattern_segment_ok = validate_scdm_edit_result(
|
||||
{
|
||||
"ok": True,
|
||||
"output_step": str(output_step),
|
||||
"result": {
|
||||
"applied": {
|
||||
"segmentSpacing": 6.5,
|
||||
"targetSpacing": 6.5,
|
||||
"segmentIndex": 1,
|
||||
"spacingMode": "segment_after",
|
||||
}
|
||||
},
|
||||
},
|
||||
before_signature=before_pattern_segment["objects"][0]["geometrySignature"], # type: ignore[index]
|
||||
before_cache=before_pattern_segment,
|
||||
after_cache=after_pattern_segment,
|
||||
capability_key="pattern.segment_spacing",
|
||||
expected_target=6.5,
|
||||
edited_object_id="pattern:holes",
|
||||
)
|
||||
_assert(
|
||||
pattern_segment_ok.get("ok") is True
|
||||
and pattern_segment_ok.get("targetCheck", {}).get("spacingMode") == "segment_after",
|
||||
f"pattern.segment_spacing should validate from the applied edit result even when the old uniform pattern no longer matches: {pattern_segment_ok}",
|
||||
)
|
||||
pattern_segment_mismatch = validate_scdm_edit_result(
|
||||
{
|
||||
"ok": True,
|
||||
"output_step": str(output_step),
|
||||
"applied": {"segmentSpacing": 6.0, "segmentIndex": 1, "spacingMode": "segment_after"},
|
||||
},
|
||||
before_signature=before_pattern_segment["objects"][0]["geometrySignature"], # type: ignore[index]
|
||||
before_cache=before_pattern_segment,
|
||||
after_cache=after_pattern_segment,
|
||||
capability_key="pattern.segment_spacing",
|
||||
expected_target=6.5,
|
||||
edited_object_id="pattern:holes",
|
||||
)
|
||||
_assert(
|
||||
pattern_segment_mismatch.get("ok") is False and pattern_segment_mismatch.get("reason") == "target-mismatch",
|
||||
f"pattern.segment_spacing mismatch should fail from the applied edit result: {pattern_segment_mismatch}",
|
||||
)
|
||||
shell_thickness_ok = check_scdm_target(
|
||||
{
|
||||
"objectType": "thin_wall",
|
||||
"geometrySignature": {"objectType": "thin_wall", "thickness": 1.6},
|
||||
"capabilities": [{"key": "shell.thickness", "currentValue": 1.6}],
|
||||
},
|
||||
capability_key="shell.thickness",
|
||||
expected_target=1.6,
|
||||
)
|
||||
_assert(shell_thickness_ok.get("ok") is True, f"shell.thickness result should validate target thickness: {shell_thickness_ok}")
|
||||
shell_thickness_mismatch = check_scdm_target(
|
||||
{
|
||||
"objectType": "thin_wall",
|
||||
"geometrySignature": {"objectType": "thin_wall", "thickness": 1.6},
|
||||
"capabilities": [{"key": "shell.thickness", "currentValue": 1.6}],
|
||||
},
|
||||
capability_key="shell.thickness",
|
||||
expected_target=2.0,
|
||||
)
|
||||
_assert(shell_thickness_mismatch.get("ok") is False and shell_thickness_mismatch.get("reason") == "target-mismatch", f"shell.thickness mismatch should fail: {shell_thickness_mismatch}")
|
||||
|
||||
summary_before = _cache_with_summary(
|
||||
{"bodyCount": 13, "objectCount": 554, "faceCount": 158, "edgeCount": 396},
|
||||
_hole("hole:85", 85, diameter=0.5, center=(0.5, 1.0, 9.5)),
|
||||
@@ -154,6 +590,57 @@ def main() -> int:
|
||||
summary_fill = check_scdm_summary_delta(summary_before, summary_after_bad, capability_key="feature.fill")
|
||||
_assert(summary_fill.get("ok") is None and summary_fill.get("reason") == "skipped-command-feature", f"fill should skip summary count guard: {summary_fill}")
|
||||
|
||||
fill_before = _cache(
|
||||
_hole("hole:85", 85, diameter=0.5, center=(0.5, 1.0, 9.5)),
|
||||
_hole("hole:87", 87, diameter=0.5, center=(2.0, 1.0, 9.5)),
|
||||
)
|
||||
fill_after = _cache(_hole("hole:87", 87, diameter=0.5, center=(2.0, 1.0, 9.5)))
|
||||
fill_signature = fill_before["objects"][0]["geometrySignature"] # type: ignore[index]
|
||||
fill_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=fill_signature,
|
||||
before_cache=fill_before,
|
||||
after_cache=fill_after,
|
||||
capability_key="feature.fill",
|
||||
edited_object_id="hole:85",
|
||||
)
|
||||
_assert(fill_ok.get("ok") is True, f"feature.fill should pass when the edited feature disappears: {fill_ok}")
|
||||
_assert(fill_ok.get("removalCheck", {}).get("ok") is True, f"feature.fill should record removal evidence: {fill_ok}")
|
||||
fill_still_present = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=fill_signature,
|
||||
before_cache=fill_before,
|
||||
after_cache=fill_before,
|
||||
capability_key="feature.fill",
|
||||
edited_object_id="hole:85",
|
||||
)
|
||||
_assert(
|
||||
fill_still_present.get("ok") is False and fill_still_present.get("reason") == "feature-still-present",
|
||||
f"feature.fill should fail when the edited feature still matches: {fill_still_present}",
|
||||
)
|
||||
fill_no_cache = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=fill_signature,
|
||||
capability_key="feature.fill",
|
||||
)
|
||||
_assert(
|
||||
fill_no_cache.get("ok") is False and fill_no_cache.get("reason") == "removal-check-unavailable",
|
||||
f"feature.fill should require a new cache for removal verification: {fill_no_cache}",
|
||||
)
|
||||
|
||||
round_before = _cache(_feature("round:60", "round", 60, center=(1.0, 0.0, 2.0), capability_key="feature.delete_round_or_chamfer"))
|
||||
round_after = _cache(_feature("hole:85", "hole", 85, center=(5.0, 0.0, 2.0), capability_key="hole.diameter"))
|
||||
round_delete_ok = validate_scdm_edit_result(
|
||||
{"ok": True, "output_step": str(output_step)},
|
||||
before_signature=round_before["objects"][0]["geometrySignature"], # type: ignore[index]
|
||||
before_cache=round_before,
|
||||
after_cache=round_after,
|
||||
capability_key="feature.delete_round_or_chamfer",
|
||||
edited_object_id="round:60",
|
||||
)
|
||||
_assert(round_delete_ok.get("ok") is True, f"round/chamfer delete should pass when the edited feature disappears: {round_delete_ok}")
|
||||
_assert(round_delete_ok.get("targetCheck", {}).get("reason") == "removed", f"round/chamfer delete should use removal target check: {round_delete_ok}")
|
||||
|
||||
ambiguous_after = _cache(
|
||||
_hole("hole:100", 100, diameter=0.75, center=(0.5, 1.0, 9.5)),
|
||||
_hole("hole:101", 101, diameter=0.75, center=(0.5, 1.0, 9.5)),
|
||||
|
||||
@@ -100,8 +100,8 @@ def main() -> int:
|
||||
"confidence": "low",
|
||||
},
|
||||
{
|
||||
"capabilityKey": "pattern.spacing",
|
||||
"displayName": "阵列间距",
|
||||
"capabilityKey": "pattern.instance_position",
|
||||
"displayName": "阵列实例位置",
|
||||
"evidenceCount": 2,
|
||||
"confidence": "low",
|
||||
},
|
||||
@@ -114,9 +114,7 @@ def main() -> int:
|
||||
}
|
||||
],
|
||||
"planned_not_productized": [
|
||||
{"capabilityKey": "slot.width", "objectType": "slot"},
|
||||
{"capabilityKey": "slot.width", "objectType": "slot"},
|
||||
{"capabilityKey": "round.radius", "objectType": "round"},
|
||||
{"capabilityKey": "shell.thickness", "objectType": "shell"},
|
||||
],
|
||||
"discovered_not_productized": [
|
||||
{"objectType": "mystery_feature"},
|
||||
@@ -126,14 +124,30 @@ def main() -> int:
|
||||
}
|
||||
progress = summarize_scdm_capability_progress(
|
||||
feature_cache=progress_cache,
|
||||
execution_ready={"face.offset", "hole.diameter", "hole.position", "slot.position", "boss.position"},
|
||||
execution_ready={
|
||||
"face.offset",
|
||||
"hole.diameter",
|
||||
"hole.position",
|
||||
"feature.fill",
|
||||
"slot.width",
|
||||
"slot.depth",
|
||||
"slot.position",
|
||||
"boss.diameter",
|
||||
"boss.height",
|
||||
"boss.position",
|
||||
"round.radius",
|
||||
"chamfer.distance",
|
||||
"feature.delete_round_or_chamfer",
|
||||
"pattern.spacing",
|
||||
"pattern.segment_spacing",
|
||||
},
|
||||
)
|
||||
summary = progress.get("summary")
|
||||
_assert(isinstance(summary, dict), f"capability progress should include summary: {progress}")
|
||||
_assert(summary.get("productized") == 6, f"productized capability count should include S5 plus Move-based S7 entries: {summary}")
|
||||
_assert(summary.get("runnerReady") == 5, f"runner-ready capability count should honor UI gate: {summary}")
|
||||
_assert(summary.get("executableCapabilities") == 2, f"blocked/ungated capabilities should not be executable: {summary}")
|
||||
_assert(summary.get("plannedDetected") == 3, f"planned S7 detections should be counted: {summary}")
|
||||
_assert(summary.get("productized") == 15, f"productized capability count should include S5 plus slot dimensions, boss dimensions, round/chamfer dimensions, pattern spacing, local segment spacing, Move-based S7 entries and round/chamfer delete: {summary}")
|
||||
_assert(summary.get("runnerReady") == 15, f"runner-ready capability count should honor UI gate: {summary}")
|
||||
_assert(summary.get("executableCapabilities") == 3, f"blocked/ungated capabilities should not be executable: {summary}")
|
||||
_assert(summary.get("plannedDetected") == 1, f"planned S7 detections should be counted: {summary}")
|
||||
_assert(summary.get("discoveredNotProductized") == 2, f"unknown discoveries should be counted: {summary}")
|
||||
_assert(summary.get("faceAdjacency") == 2 and summary.get("circularEdges") == 4, f"probe topology evidence should be counted: {summary}")
|
||||
_assert(
|
||||
@@ -147,9 +161,16 @@ def main() -> int:
|
||||
evidence_lines = "\n".join(str(line) for line in progress.get("probeEvidence", {}).get("lines", []))
|
||||
_assert("偏移:已开放" in productized_lines, f"open SCDM capability should be visible: {productized_lines}")
|
||||
_assert("直径:已开放但被后端阻止" in productized_lines, f"blocked SCDM capability should be explicit: {productized_lines}")
|
||||
_assert("填孔/删除小特征:已识别待执行器" in productized_lines, f"recognized but ungated capability should be visible: {productized_lines}")
|
||||
_assert("槽宽:已识别待验证" in planned_lines, f"planned S7 candidate should be visible: {planned_lines}")
|
||||
_assert("凸台高度:几何证据待分类" in planned_lines, f"geometry-only S7 hint should be visible: {planned_lines}")
|
||||
_assert("填孔/删除小特征:已开放" in productized_lines, f"recognized command capability should be visible as open: {productized_lines}")
|
||||
_assert("槽深:已开放待识别" in productized_lines, f"productized slot.depth should be visible: {productized_lines}")
|
||||
_assert("凸台直径:已开放待识别" in productized_lines, f"productized boss.diameter should be visible: {productized_lines}")
|
||||
_assert("凸台高度:已开放待识别" in productized_lines, f"productized boss.height should be visible: {productized_lines}")
|
||||
_assert("圆角半径:已开放待识别" in productized_lines, f"productized round.radius should be visible: {productized_lines}")
|
||||
_assert("倒角距离:已开放待识别" in productized_lines, f"productized chamfer.distance should be visible: {productized_lines}")
|
||||
_assert("阵列间距:已开放待识别" in productized_lines, f"productized pattern.spacing should be visible: {productized_lines}")
|
||||
_assert("局部间距:已开放待识别" in productized_lines, f"productized pattern.segment_spacing should be visible: {productized_lines}")
|
||||
_assert("壳体厚度:已识别待验证" in planned_lines, f"planned shell.thickness candidate should be visible: {planned_lines}")
|
||||
_assert("阵列实例位置:几何证据待分类" in planned_lines, f"geometry-only S7 hint should be visible: {planned_lines}")
|
||||
_assert("Face 邻接 2 组" in evidence_lines and "圆边 4 条" in evidence_lines, f"probe evidence lines should be readable: {evidence_lines}")
|
||||
_assert("对象分布" in evidence_lines and "命令候选分布" in evidence_lines, f"probe inventory lines should be readable: {evidence_lines}")
|
||||
_assert("几何候选 凸台高度:3" in evidence_lines, f"probe geometry hint lines should be readable: {evidence_lines}")
|
||||
|
||||
Reference in New Issue
Block a user