feat: 完善 SCDM-first 参数化编辑交付版

This commit is contained in:
2026-08-20 17:12:01 +08:00
parent 4e7877e05c
commit b4feab24d2
21 changed files with 3015 additions and 1729 deletions
+102 -4
View File
@@ -8,7 +8,7 @@ from .scdm_capabilities import capability_definition, planned_capability_keys, p
from .scdm_schema import SCDM_CACHE_SCHEMA_VERSION, payload_backend_version, payload_model_fingerprint, read_json, utc_now, write_json
SCDM_FEATURE_CACHE_REVISION = 4
SCDM_FEATURE_CACHE_REVISION = 8
def map_scdm_raw_features(raw_payload: Mapping[str, object]) -> dict[str, object]:
@@ -400,6 +400,8 @@ def _cylindrical_face_group_objects(raw_objects: list[object]) -> list[dict[str,
for raw_object in raw_objects:
if not isinstance(raw_object, Mapping):
continue
if not _can_derive_cylindrical_hole_group(raw_object):
continue
geometry = _mapping(raw_object.get("geometry"))
if _surface_key(geometry.get("surfaceType") or geometry.get("surface")) != "cylinder":
continue
@@ -453,6 +455,56 @@ def _cylindrical_face_group_objects(raw_objects: list[object]) -> list[dict[str,
return result
def _can_derive_cylindrical_hole_group(raw_object: Mapping[str, object]) -> bool:
object_type = str(raw_object.get("objectType") or "").strip().lower()
if object_type in {
"round",
"fillet",
"chamfer",
"slot",
"obround_slot",
"rectangular_slot",
"boss",
"cylindrical_boss",
"rectangular_boss",
"shell",
"thin_wall",
}:
return False
if object_type not in {"", "face", "planar_face", "hole", "cylindrical_hole"}:
return False
geometry = _mapping(raw_object.get("geometry"))
if _mapping(geometry.get("roundInfo")) or _mapping(geometry.get("chamferInfo")) or _mapping(geometry.get("slotInfo")):
return object_type in {"hole", "cylindrical_hole"}
commands = tuple(_raw_command_tokens(raw_object.get("backendCommandCandidates")))
if any(token in command for command in commands for token in ("round", "fillet", "chamfer", "slot", "boss")):
return object_type in {"hole", "cylindrical_hole"}
return True
def _raw_command_tokens(value: object) -> list[str]:
if not isinstance(value, list):
return []
tokens: list[str] = []
for item in value:
if not isinstance(item, Mapping):
continue
if item.get("enabled") is False:
continue
tokens.append(
" ".join(
str(part or "")
for part in (
item.get("key"),
item.get("operation"),
item.get("command"),
item.get("type"),
)
).lower()
)
return tokens
def _derived_feature_objects(source_objects: list[object]) -> list[dict[str, object]]:
return [
*_derived_linear_pattern_objects(source_objects),
@@ -952,7 +1004,7 @@ def _derived_thin_wall_objects(source_objects: list[object]) -> list[dict[str, o
{"operation": "change_shell_thickness", "enabled": True, "parameterFields": {"thickness": round(float(candidate["thickness"]), 6)}},
],
"rawLimitations": [
"Derived from paired planar SCDM faces; shell.thickness remains non-productized until real SCDM edit samples pass.",
"Derived from paired planar SCDM faces; shell.thickness is productized only when both wall faces and thickness axis are locatable.",
],
}
)
@@ -1153,6 +1205,8 @@ def attach_local_face_ids_to_scdm_cache(
item = dict(raw_object)
signature = dict(item.get("geometrySignature") if isinstance(item.get("geometrySignature"), Mapping) else {})
if not _int_list(signature.get("faceIds")):
# SCDM 和本软件的 Face 编号不是同一套 ID。优先用 ordinal locator 精确映射;
# locator 不完整时才退到几何签名打分,避免修改后 ID 变化把公式续接错。
ordinal_face_ids = _face_ids_from_signature_ordinals(signature, local_signatures)
if ordinal_face_ids:
signature["faceIds"] = ordinal_face_ids
@@ -1195,6 +1249,8 @@ def attach_local_face_ids_to_scdm_cache(
support_face_ids = _pattern_support_face_ids(signature, local_signatures)
if support_face_ids:
signature["supportFaceIds"] = support_face_ids
# 阵列间距不能只看实例中心距,还要知道这些实例是否仍落在支撑面上。
# supportPatternFit 会给 UI 和 preflight 提供最大安全间距。
support_fit = _pattern_support_fit(signature, local_signatures, support_face_ids, unit_scale or 1.0)
if support_fit:
signature["supportPatternFit"] = support_fit
@@ -1420,6 +1476,8 @@ def _pattern_support_face_ids(
if distance > tolerance:
continue
area = _number(local.get("area")) or _box_area_estimate(face_box)
# 支撑面候选要求和阵列包围盒投影重叠,并且沿面法向几乎贴合;
# 面积越大、贴合越近,越像承载该阵列的底面。
candidates.append((overlap * max(area, 1.0) - distance, int(face_id)))
candidates.sort(reverse=True)
return [face_id for _score, face_id in candidates[:4]]
@@ -1469,6 +1527,8 @@ def _pattern_support_fit(
max_spacing_local = (2.0 * min(left_capacity, right_capacity)) / (len(center_projections) - 1)
if max_spacing_local <= 0:
continue
# 这里按“保持整列中心不变”等距重排”估算最大间距。
# 局部段间距会在 property spec 里按具体移动语义再计算范围。
support_candidates.append(
{
"faceId": int(face_id),
@@ -1742,20 +1802,58 @@ def _current_value(fields: tuple[str, ...], raw_object: Mapping[str, object]) ->
return 1
if field == "geometry.radius*2":
radius = _number(geometry.get("radius"))
if radius is not None:
if radius is not None and radius > 0:
return radius * 2.0
continue
if field.startswith("geometry."):
value = _path_value(geometry, field.split(".", 1)[1])
if value is not None:
if _invalid_dimension_current_value(field, value):
continue
return value
return None
def _invalid_dimension_current_value(field: str, value: object) -> bool:
if not field.startswith("geometry."):
return False
path = field.split(".", 1)[1]
if path in {"offset", "planeOffset"} or path.endswith(".offset"):
return False
dimension_suffixes = (
"radius",
"diameter",
"width",
"depth",
"height",
"distance",
"thickness",
"spacing",
"pitch",
)
if not path.endswith(dimension_suffixes):
return False
number = _number(value)
return number is not None and number <= 0
def _missing_geometry_reason(key: str, raw_object: Mapping[str, object]) -> str:
if key not in {"slot.depth", "boss.height", "boss.diameter", "round.radius", "chamfer.distance", "pattern.spacing"}:
if key not in {"slot.depth", "boss.height", "boss.diameter", "round.radius", "chamfer.distance", "pattern.spacing", "shell.thickness"}:
return ""
signature = geometry_signature(raw_object)
if key == "shell.thickness":
thickness = _rounded_number(signature.get("thickness"))
if thickness is None or thickness <= 0:
return "SCDM 已识别薄壁候选,但没有返回可用于编辑的当前壳体厚度。"
axis = _rounded_vector(signature.get("thicknessAxis") or signature.get("axis"))
if len(axis) != 3:
return "SCDM 已识别薄壁厚度,但没有返回稳定的厚度方向,暂不能修改。"
wall_locators = _face_locators(signature.get("wallFaceLocators") or signature.get("scdmFaceLocators"))
wall_ordinals = _int_list(signature.get("wallFaceOrdinals") or signature.get("faceOrdinals"))
wall_global_ordinals = _int_list(signature.get("globalWallFaceOrdinals") or signature.get("globalFaceOrdinals"))
if len(wall_locators) < 2 and len(wall_ordinals) < 2 and len(wall_global_ordinals) < 2:
return "SCDM 已识别薄壁厚度,但没有返回两侧墙面的定位信息,暂不能修改。"
return ""
if key == "pattern.spacing":
spacing = _rounded_number(_first_present(signature.get("spacing"), signature.get("pitch")))
if spacing is None or spacing <= 0: