2026-08-19 10:28:09 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from collections.abc import Mapping
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class ScdmCapabilityDefinition:
|
|
|
|
|
key: str
|
|
|
|
|
display_name: str
|
|
|
|
|
object_types: tuple[str, ...]
|
|
|
|
|
value_kind: str
|
|
|
|
|
current_fields: tuple[str, ...]
|
|
|
|
|
default_intent: str
|
|
|
|
|
backend_operation: str
|
|
|
|
|
post_check: str
|
|
|
|
|
required_backend_command_groups: tuple[tuple[str, ...], ...] = ()
|
|
|
|
|
productized: bool = True
|
|
|
|
|
roadmap_stage: str = "S5"
|
|
|
|
|
block_reason: str = ""
|
|
|
|
|
|
|
|
|
|
def to_payload(self) -> dict[str, object]:
|
|
|
|
|
return {
|
|
|
|
|
"key": self.key,
|
|
|
|
|
"displayName": self.display_name,
|
|
|
|
|
"objectTypes": self.object_types,
|
|
|
|
|
"valueKind": self.value_kind,
|
|
|
|
|
"currentFields": self.current_fields,
|
|
|
|
|
"defaultIntent": self.default_intent,
|
|
|
|
|
"backendOperation": self.backend_operation,
|
|
|
|
|
"postCheck": self.post_check,
|
|
|
|
|
"requiredBackendCommandGroups": self.required_backend_command_groups,
|
|
|
|
|
"productized": self.productized,
|
|
|
|
|
"roadmapStage": self.roadmap_stage,
|
|
|
|
|
"blockReason": self.block_reason,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CAPABILITY_DEFINITIONS: dict[str, ScdmCapabilityDefinition] = {
|
|
|
|
|
"hole.diameter": ScdmCapabilityDefinition(
|
|
|
|
|
key="hole.diameter",
|
|
|
|
|
display_name="直径",
|
|
|
|
|
object_types=("hole", "cylindrical_hole", "cylindrical_face_group"),
|
|
|
|
|
value_kind="number",
|
|
|
|
|
current_fields=("geometry.diameter", "geometry.radius*2"),
|
|
|
|
|
default_intent="修改孔径",
|
|
|
|
|
backend_operation="change_hole_diameter",
|
|
|
|
|
post_check="target_hole_diameter",
|
|
|
|
|
required_backend_command_groups=(("StandardHoles",), ("OffsetFaces",)),
|
|
|
|
|
roadmap_stage="S5",
|
|
|
|
|
),
|
|
|
|
|
"hole.position": ScdmCapabilityDefinition(
|
|
|
|
|
key="hole.position",
|
|
|
|
|
display_name="位置",
|
|
|
|
|
object_types=("hole", "cylindrical_hole", "cylindrical_face_group"),
|
|
|
|
|
value_kind="vector3",
|
|
|
|
|
current_fields=("geometry.center", "geometry.axisCenter"),
|
|
|
|
|
default_intent="移动孔",
|
|
|
|
|
backend_operation="move_hole_axis",
|
|
|
|
|
post_check="target_hole_axis_center",
|
|
|
|
|
required_backend_command_groups=(("Move",),),
|
|
|
|
|
roadmap_stage="S5",
|
|
|
|
|
),
|
|
|
|
|
"face.offset": ScdmCapabilityDefinition(
|
|
|
|
|
key="face.offset",
|
|
|
|
|
display_name="偏移",
|
|
|
|
|
object_types=("face", "planar_face"),
|
|
|
|
|
value_kind="number",
|
|
|
|
|
current_fields=("geometry.offset", "geometry.planeOffset", "0"),
|
|
|
|
|
default_intent="推拉平面",
|
|
|
|
|
backend_operation="pull_face_offset",
|
|
|
|
|
post_check="target_face_offset",
|
|
|
|
|
required_backend_command_groups=(("OffsetFaces",),),
|
|
|
|
|
roadmap_stage="S5",
|
|
|
|
|
),
|
|
|
|
|
"feature.fill": ScdmCapabilityDefinition(
|
|
|
|
|
key="feature.fill",
|
|
|
|
|
display_name="填孔/删除小特征",
|
|
|
|
|
object_types=("hole", "small_feature"),
|
|
|
|
|
value_kind="command",
|
|
|
|
|
current_fields=("1",),
|
|
|
|
|
default_intent="删除并补面",
|
|
|
|
|
backend_operation="fill_feature",
|
|
|
|
|
post_check="target_feature_removed",
|
|
|
|
|
required_backend_command_groups=(("Fill",), ("Delete",)),
|
|
|
|
|
roadmap_stage="S5",
|
|
|
|
|
),
|
|
|
|
|
"slot.width": ScdmCapabilityDefinition(
|
|
|
|
|
key="slot.width",
|
|
|
|
|
display_name="槽宽",
|
|
|
|
|
object_types=("slot", "obround_slot", "rectangular_slot"),
|
|
|
|
|
value_kind="number",
|
|
|
|
|
current_fields=("geometry.width",),
|
|
|
|
|
default_intent="修改槽宽",
|
|
|
|
|
backend_operation="change_slot_width",
|
|
|
|
|
post_check="target_slot_width",
|
2026-08-19 18:02:47 +08:00
|
|
|
required_backend_command_groups=(("OffsetFaces",),),
|
2026-08-19 10:28:09 +08:00
|
|
|
roadmap_stage="S7.2",
|
|
|
|
|
),
|
|
|
|
|
"slot.depth": ScdmCapabilityDefinition(
|
|
|
|
|
key="slot.depth",
|
|
|
|
|
display_name="槽深",
|
|
|
|
|
object_types=("slot", "obround_slot", "rectangular_slot"),
|
|
|
|
|
value_kind="number",
|
2026-08-19 18:02:47 +08:00
|
|
|
current_fields=("geometry.slotInfo.depth", "geometry.depth"),
|
2026-08-19 10:28:09 +08:00
|
|
|
default_intent="修改槽深",
|
|
|
|
|
backend_operation="change_slot_depth",
|
|
|
|
|
post_check="target_slot_depth",
|
2026-08-19 18:02:47 +08:00
|
|
|
required_backend_command_groups=(("Move",), ("OffsetFaces",)),
|
2026-08-19 10:28:09 +08:00
|
|
|
roadmap_stage="S7.2",
|
|
|
|
|
),
|
|
|
|
|
"slot.position": ScdmCapabilityDefinition(
|
|
|
|
|
key="slot.position",
|
|
|
|
|
display_name="槽位置",
|
|
|
|
|
object_types=("slot", "obround_slot", "rectangular_slot"),
|
|
|
|
|
value_kind="vector3",
|
|
|
|
|
current_fields=("geometry.center", "geometry.axisCenter"),
|
|
|
|
|
default_intent="移动槽",
|
|
|
|
|
backend_operation="move_slot",
|
|
|
|
|
post_check="target_slot_center",
|
|
|
|
|
required_backend_command_groups=(("Move",),),
|
|
|
|
|
roadmap_stage="S7.2",
|
|
|
|
|
),
|
|
|
|
|
"boss.height": ScdmCapabilityDefinition(
|
|
|
|
|
key="boss.height",
|
|
|
|
|
display_name="凸台高度",
|
|
|
|
|
object_types=("boss", "cylindrical_boss", "rectangular_boss"),
|
|
|
|
|
value_kind="number",
|
|
|
|
|
current_fields=("geometry.height",),
|
|
|
|
|
default_intent="修改凸台高度",
|
|
|
|
|
backend_operation="change_boss_height",
|
|
|
|
|
post_check="target_boss_height",
|
2026-08-19 18:02:47 +08:00
|
|
|
required_backend_command_groups=(("Move",), ("OffsetFaces",)),
|
2026-08-19 10:28:09 +08:00
|
|
|
roadmap_stage="S7.3",
|
|
|
|
|
),
|
|
|
|
|
"boss.diameter": ScdmCapabilityDefinition(
|
|
|
|
|
key="boss.diameter",
|
|
|
|
|
display_name="凸台直径",
|
|
|
|
|
object_types=("boss", "cylindrical_boss"),
|
|
|
|
|
value_kind="number",
|
|
|
|
|
current_fields=("geometry.diameter", "geometry.radius*2"),
|
|
|
|
|
default_intent="修改凸台直径",
|
|
|
|
|
backend_operation="change_boss_diameter",
|
|
|
|
|
post_check="target_boss_diameter",
|
2026-08-19 18:02:47 +08:00
|
|
|
required_backend_command_groups=(("OffsetFaces",),),
|
2026-08-19 10:28:09 +08:00
|
|
|
roadmap_stage="S7.3",
|
|
|
|
|
),
|
|
|
|
|
"boss.position": ScdmCapabilityDefinition(
|
|
|
|
|
key="boss.position",
|
|
|
|
|
display_name="凸台位置",
|
|
|
|
|
object_types=("boss", "cylindrical_boss", "rectangular_boss"),
|
|
|
|
|
value_kind="vector3",
|
|
|
|
|
current_fields=("geometry.center", "geometry.axisCenter"),
|
|
|
|
|
default_intent="移动凸台",
|
|
|
|
|
backend_operation="move_boss",
|
|
|
|
|
post_check="target_boss_center",
|
|
|
|
|
required_backend_command_groups=(("Move",),),
|
|
|
|
|
roadmap_stage="S7.3",
|
|
|
|
|
),
|
|
|
|
|
"round.radius": ScdmCapabilityDefinition(
|
|
|
|
|
key="round.radius",
|
|
|
|
|
display_name="圆角半径",
|
|
|
|
|
object_types=("round", "fillet"),
|
|
|
|
|
value_kind="number",
|
2026-08-19 18:02:47 +08:00
|
|
|
current_fields=("geometry.roundInfo.radius", "geometry.radius"),
|
2026-08-19 10:28:09 +08:00
|
|
|
default_intent="修改圆角半径",
|
|
|
|
|
backend_operation="change_round_radius",
|
|
|
|
|
post_check="target_round_radius",
|
|
|
|
|
required_backend_command_groups=(("ConstantRound",),),
|
|
|
|
|
roadmap_stage="S7.4",
|
|
|
|
|
),
|
|
|
|
|
"chamfer.distance": ScdmCapabilityDefinition(
|
|
|
|
|
key="chamfer.distance",
|
|
|
|
|
display_name="倒角距离",
|
|
|
|
|
object_types=("chamfer",),
|
|
|
|
|
value_kind="number",
|
2026-08-19 18:02:47 +08:00
|
|
|
current_fields=("geometry.chamferInfo.distance", "geometry.distance", "geometry.offset"),
|
2026-08-19 10:28:09 +08:00
|
|
|
default_intent="修改倒角距离",
|
|
|
|
|
backend_operation="change_chamfer_distance",
|
|
|
|
|
post_check="target_chamfer_distance",
|
|
|
|
|
required_backend_command_groups=(("Chamfer",),),
|
|
|
|
|
roadmap_stage="S7.4",
|
|
|
|
|
),
|
|
|
|
|
"feature.delete_round_or_chamfer": ScdmCapabilityDefinition(
|
|
|
|
|
key="feature.delete_round_or_chamfer",
|
|
|
|
|
display_name="删除圆角/倒角",
|
|
|
|
|
object_types=("round", "fillet", "chamfer"),
|
|
|
|
|
value_kind="command",
|
|
|
|
|
current_fields=("1",),
|
|
|
|
|
default_intent="删除圆角/倒角并补面",
|
|
|
|
|
backend_operation="delete_round_or_chamfer",
|
|
|
|
|
post_check="target_feature_removed",
|
|
|
|
|
required_backend_command_groups=(("Fill",), ("Delete",)),
|
|
|
|
|
roadmap_stage="S7.4",
|
|
|
|
|
),
|
|
|
|
|
"pattern.spacing": ScdmCapabilityDefinition(
|
|
|
|
|
key="pattern.spacing",
|
|
|
|
|
display_name="阵列间距",
|
|
|
|
|
object_types=("pattern", "linear_pattern"),
|
|
|
|
|
value_kind="number",
|
|
|
|
|
current_fields=("geometry.spacing", "geometry.pitch"),
|
|
|
|
|
default_intent="修改阵列间距",
|
|
|
|
|
backend_operation="change_pattern_spacing",
|
|
|
|
|
post_check="target_pattern_spacing",
|
2026-08-19 18:02:47 +08:00
|
|
|
required_backend_command_groups=(("Move",),),
|
|
|
|
|
roadmap_stage="S7.5",
|
|
|
|
|
),
|
|
|
|
|
"pattern.segment_spacing": ScdmCapabilityDefinition(
|
|
|
|
|
key="pattern.segment_spacing",
|
|
|
|
|
display_name="局部间距",
|
|
|
|
|
object_types=("pattern", "linear_pattern"),
|
|
|
|
|
value_kind="number",
|
|
|
|
|
current_fields=("geometry.spacing", "geometry.pitch"),
|
|
|
|
|
default_intent="修改相邻阵列成员间距",
|
|
|
|
|
backend_operation="change_pattern_segment_spacing",
|
|
|
|
|
post_check="target_pattern_segment_spacing",
|
|
|
|
|
required_backend_command_groups=(("Move",),),
|
2026-08-19 10:28:09 +08:00
|
|
|
roadmap_stage="S7.5",
|
|
|
|
|
),
|
|
|
|
|
"pattern.instance_position": ScdmCapabilityDefinition(
|
|
|
|
|
key="pattern.instance_position",
|
|
|
|
|
display_name="阵列实例位置",
|
|
|
|
|
object_types=("pattern", "linear_pattern"),
|
|
|
|
|
value_kind="vector3",
|
|
|
|
|
current_fields=("geometry.instanceCenter", "geometry.center"),
|
|
|
|
|
default_intent="移动阵列实例",
|
|
|
|
|
backend_operation="move_pattern_instance",
|
|
|
|
|
post_check="target_pattern_instance_center",
|
|
|
|
|
productized=False,
|
|
|
|
|
roadmap_stage="S7.5",
|
|
|
|
|
block_reason="阵列实例位置属于 S7 第五批能力,真实 SCDM 命令和 STEP 样例回测尚未完成。",
|
|
|
|
|
),
|
|
|
|
|
"shell.thickness": ScdmCapabilityDefinition(
|
|
|
|
|
key="shell.thickness",
|
|
|
|
|
display_name="壳体厚度",
|
|
|
|
|
object_types=("shell", "thin_wall"),
|
|
|
|
|
value_kind="number",
|
|
|
|
|
current_fields=("geometry.thickness",),
|
|
|
|
|
default_intent="修改壳体厚度",
|
|
|
|
|
backend_operation="change_shell_thickness",
|
|
|
|
|
post_check="target_shell_thickness",
|
|
|
|
|
productized=False,
|
|
|
|
|
roadmap_stage="S7.5",
|
|
|
|
|
block_reason="壳体厚度属于 S7 第五批能力,真实 SCDM 命令和 STEP 样例回测尚未完成。",
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def capability_definition(key: str) -> ScdmCapabilityDefinition | None:
|
|
|
|
|
return CAPABILITY_DEFINITIONS.get(key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def productized_capability_keys(raw_object: Mapping[str, object]) -> tuple[str, ...]:
|
|
|
|
|
return tuple(
|
|
|
|
|
key
|
|
|
|
|
for key in capability_keys_for_raw_object(raw_object, include_planned=False)
|
|
|
|
|
if (definition := capability_definition(key)) is not None and definition.productized
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def planned_capability_keys(raw_object: Mapping[str, object]) -> tuple[str, ...]:
|
|
|
|
|
return tuple(
|
|
|
|
|
key
|
|
|
|
|
for key in capability_keys_for_raw_object(raw_object, include_planned=True)
|
|
|
|
|
if (definition := capability_definition(key)) is not None and not definition.productized
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def capability_keys_for_raw_object(raw_object: Mapping[str, object], *, include_planned: bool = False) -> tuple[str, ...]:
|
|
|
|
|
object_type = str(raw_object.get("objectType") or "").strip().lower()
|
|
|
|
|
geometry = _mapping(raw_object.get("geometry"))
|
|
|
|
|
commands = tuple(_command_operations(raw_object.get("backendCommandCandidates")))
|
|
|
|
|
keys: list[str] = []
|
|
|
|
|
|
|
|
|
|
if object_type in {"hole", "cylindrical_hole", "cylindrical_face_group"}:
|
|
|
|
|
if _has_any(geometry, ("diameter", "radius")) or _has_command_token(commands, ("diameter", "radius")):
|
|
|
|
|
keys.append("hole.diameter")
|
|
|
|
|
if _has_any(geometry, ("center", "axisCenter")) or _has_command_token(commands, ("move", "position", "translate")):
|
|
|
|
|
keys.append("hole.position")
|
|
|
|
|
if _has_command_token(commands, ("fill", "delete", "remove")):
|
|
|
|
|
keys.append("feature.fill")
|
|
|
|
|
|
|
|
|
|
surface_type = str(geometry.get("surfaceType") or geometry.get("surface") or "").strip().lower()
|
|
|
|
|
if object_type in {"face", "planar_face"} and surface_type in {"plane", "planar", ""}:
|
|
|
|
|
if _has_command_token(commands, ("pull", "offset", "move_face")) or _has_any(geometry, ("normal", "planeOffset")):
|
|
|
|
|
keys.append("face.offset")
|
|
|
|
|
|
|
|
|
|
if object_type in {"hole", "small_feature"} and _has_command_token(commands, ("fill", "delete", "remove")):
|
|
|
|
|
keys.append("feature.fill")
|
|
|
|
|
|
|
|
|
|
if object_type in {"slot", "obround_slot", "rectangular_slot"}:
|
|
|
|
|
if _has_any(geometry, ("width",)) or _has_command_token(commands, ("slot_width", "width")):
|
|
|
|
|
keys.append("slot.width")
|
|
|
|
|
if _has_any(geometry, ("depth",)) or _has_command_token(commands, ("slot_depth", "depth")):
|
|
|
|
|
keys.append("slot.depth")
|
|
|
|
|
if _has_any(geometry, ("center", "axisCenter")) or _has_command_token(commands, ("move", "position", "translate")):
|
|
|
|
|
keys.append("slot.position")
|
|
|
|
|
|
|
|
|
|
if object_type in {"boss", "cylindrical_boss", "rectangular_boss"}:
|
|
|
|
|
if _has_any(geometry, ("height",)) or _has_command_token(commands, ("boss_height", "height")):
|
|
|
|
|
keys.append("boss.height")
|
|
|
|
|
if object_type != "rectangular_boss" and (_has_any(geometry, ("diameter", "radius")) or _has_command_token(commands, ("diameter", "radius"))):
|
|
|
|
|
keys.append("boss.diameter")
|
|
|
|
|
if _has_any(geometry, ("center", "axisCenter")) or _has_command_token(commands, ("move", "position", "translate")):
|
|
|
|
|
keys.append("boss.position")
|
|
|
|
|
|
|
|
|
|
if object_type in {"round", "fillet"}:
|
|
|
|
|
if _has_any(geometry, ("radius",)) or _has_command_token(commands, ("round_radius", "fillet_radius", "radius")):
|
|
|
|
|
keys.append("round.radius")
|
|
|
|
|
if _has_command_token(commands, ("fill", "delete", "remove")):
|
|
|
|
|
keys.append("feature.delete_round_or_chamfer")
|
|
|
|
|
|
|
|
|
|
if object_type == "chamfer":
|
|
|
|
|
if _has_any(geometry, ("distance", "offset")) or _has_command_token(commands, ("chamfer_distance", "distance", "offset")):
|
|
|
|
|
keys.append("chamfer.distance")
|
|
|
|
|
if _has_command_token(commands, ("fill", "delete", "remove")):
|
|
|
|
|
keys.append("feature.delete_round_or_chamfer")
|
|
|
|
|
|
|
|
|
|
if object_type in {"pattern", "linear_pattern"}:
|
|
|
|
|
if _has_any(geometry, ("spacing", "pitch")) or _has_command_token(commands, ("pattern_spacing", "spacing", "pitch")):
|
|
|
|
|
keys.append("pattern.spacing")
|
2026-08-19 18:02:47 +08:00
|
|
|
keys.append("pattern.segment_spacing")
|
2026-08-19 10:28:09 +08:00
|
|
|
if _has_any(geometry, ("instanceCenter", "center")) or _has_command_token(commands, ("move_instance", "instance_position")):
|
|
|
|
|
keys.append("pattern.instance_position")
|
|
|
|
|
|
|
|
|
|
if object_type in {"shell", "thin_wall"}:
|
|
|
|
|
if _has_any(geometry, ("thickness",)) or _has_command_token(commands, ("shell_thickness", "thickness")):
|
|
|
|
|
keys.append("shell.thickness")
|
|
|
|
|
|
|
|
|
|
result = []
|
|
|
|
|
for key in keys:
|
|
|
|
|
definition = capability_definition(key)
|
|
|
|
|
if definition is None:
|
|
|
|
|
continue
|
|
|
|
|
if definition.productized or include_planned:
|
|
|
|
|
result.append(key)
|
|
|
|
|
return tuple(dict.fromkeys(result))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _mapping(value: object) -> Mapping[str, object]:
|
|
|
|
|
return value if isinstance(value, Mapping) else {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _has_any(mapping: Mapping[str, object], names: tuple[str, ...]) -> bool:
|
|
|
|
|
return any(name in mapping and mapping.get(name) is not None for name in names)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _command_operations(value: object) -> list[str]:
|
|
|
|
|
if not isinstance(value, list):
|
|
|
|
|
return []
|
|
|
|
|
result: list[str] = []
|
|
|
|
|
for item in value:
|
|
|
|
|
if not isinstance(item, Mapping):
|
|
|
|
|
continue
|
|
|
|
|
enabled = item.get("enabled")
|
|
|
|
|
if enabled is False:
|
|
|
|
|
continue
|
|
|
|
|
text = " ".join(
|
|
|
|
|
str(part or "")
|
|
|
|
|
for part in (
|
|
|
|
|
item.get("key"),
|
|
|
|
|
item.get("operation"),
|
|
|
|
|
item.get("command"),
|
|
|
|
|
item.get("type"),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
result.append(text.lower())
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _has_command_token(commands: tuple[str, ...], tokens: tuple[str, ...]) -> bool:
|
|
|
|
|
return any(token in command for command in commands for token in tokens)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__all__ = [
|
|
|
|
|
"CAPABILITY_DEFINITIONS",
|
|
|
|
|
"ScdmCapabilityDefinition",
|
|
|
|
|
"capability_keys_for_raw_object",
|
|
|
|
|
"capability_definition",
|
|
|
|
|
"planned_capability_keys",
|
|
|
|
|
"productized_capability_keys",
|
|
|
|
|
]
|