from __future__ import annotations import math from collections.abc import Iterable, Mapping from pathlib import Path from .scdm_capabilities import capability_definition, planned_capability_keys, productized_capability_keys 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 def map_scdm_raw_features(raw_payload: Mapping[str, object]) -> dict[str, object]: objects = [] diagnostics: dict[str, object] = { "discovered_not_productized": [], "planned_not_productized": [], "blocked": [], } raw_diagnostics = _mapping(raw_payload.get("diagnostics")) available_commands = raw_diagnostics.get("availableCommands") available_command_names = _available_command_names(available_commands) if isinstance(available_commands, list): diagnostics["backend_commands"] = [dict(item) for item in available_commands if isinstance(item, Mapping)] face_adjacency = raw_diagnostics.get("faceAdjacency") if isinstance(face_adjacency, list): diagnostics["face_adjacency"] = [dict(item) for item in face_adjacency if isinstance(item, Mapping)] edge_geometry_summary = raw_diagnostics.get("edgeGeometrySummary") if isinstance(edge_geometry_summary, Mapping): diagnostics["edge_geometry_summary"] = dict(edge_geometry_summary) feature_inventory = raw_diagnostics.get("featureInventory") if isinstance(feature_inventory, Mapping): diagnostics["feature_inventory"] = { str(key): dict(value) for key, value in feature_inventory.items() if isinstance(value, Mapping) } component_instances = raw_diagnostics.get("componentInstances") if isinstance(component_instances, list): diagnostics["component_instances"] = [dict(item) for item in component_instances if isinstance(item, Mapping)] raw_summary = raw_payload.get("summary") if isinstance(raw_summary, Mapping): diagnostics["raw_summary"] = dict(raw_summary) raw_objects = raw_payload.get("objects") if not isinstance(raw_objects, list): raw_objects = [] cylindrical_group_objects = _cylindrical_face_group_objects(raw_objects) derived_objects = _derived_feature_objects([*raw_objects, *cylindrical_group_objects]) if derived_objects: diagnostics["derived_feature_candidates"] = [ { "objectId": _object_id(item), "objectType": str(item.get("objectType") or ""), "geometrySignature": geometry_signature(item), "rawLimitations": list(item.get("rawLimitations") or []), } for item in derived_objects if isinstance(item, Mapping) ] diagnostics["geometry_candidate_hints"] = _geometry_candidate_hints( [*raw_objects, *cylindrical_group_objects, *derived_objects], raw_diagnostics, ) for raw_object in [*raw_objects, *derived_objects]: if not isinstance(raw_object, Mapping): continue capability_keys = productized_capability_keys(raw_object) planned_keys = planned_capability_keys(raw_object) capabilities = [_capability_payload(key, raw_object, available_command_names=available_command_names) for key in capability_keys] capabilities = [item for item in capabilities if item is not None] normalized_object = { "objectId": _object_id(raw_object), "objectType": str(raw_object.get("objectType") or ""), "sourceBackendId": str(raw_object.get("backendId") or ""), "geometrySignature": geometry_signature(raw_object), "capabilities": capabilities, "blockReason": str(raw_object.get("blockReason") or ""), } if capabilities: objects.append(normalized_object) else: diagnostics["discovered_not_productized"].append( { "objectId": normalized_object["objectId"], "objectType": normalized_object["objectType"], "sourceBackendId": normalized_object["sourceBackendId"], "reason": "no_productized_capability", } ) for key in planned_keys: definition = capability_definition(key) diagnostics["planned_not_productized"].append( { "objectId": normalized_object["objectId"], "objectType": normalized_object["objectType"], "sourceBackendId": normalized_object["sourceBackendId"], "capabilityKey": key, "displayName": definition.display_name if definition else key, "roadmapStage": definition.roadmap_stage if definition else "", "reason": definition.block_reason if definition else "Planned capability is not productized.", } ) if normalized_object["blockReason"]: diagnostics["blocked"].append( { "objectId": normalized_object["objectId"], "objectType": normalized_object["objectType"], "reason": normalized_object["blockReason"], } ) for raw_object in cylindrical_group_objects: capability_keys = productized_capability_keys(raw_object) capabilities = [_capability_payload(key, raw_object, available_command_names=available_command_names) for key in capability_keys] capabilities = [item for item in capabilities if item is not None] normalized_object = { "objectId": _object_id(raw_object), "objectType": str(raw_object.get("objectType") or ""), "sourceBackendId": str(raw_object.get("backendId") or ""), "geometrySignature": geometry_signature(raw_object), "capabilities": capabilities, "blockReason": str(raw_object.get("blockReason") or ""), } if capabilities: objects.append(normalized_object) else: diagnostics["discovered_not_productized"].append( { "objectId": normalized_object["objectId"], "objectType": normalized_object["objectType"], "sourceBackendId": normalized_object["sourceBackendId"], "reason": "no_productized_capability", } ) return { "schemaVersion": SCDM_CACHE_SCHEMA_VERSION, "mapperRevision": SCDM_FEATURE_CACHE_REVISION, "source": "SCDM", "createdAt": utc_now(), "modelFingerprint": payload_model_fingerprint(raw_payload), "backendVersion": payload_backend_version(raw_payload), "objects": objects, "diagnostics": diagnostics, } def _geometry_candidate_hints( raw_objects: list[object], raw_diagnostics: Mapping[str, object], ) -> list[dict[str, object]]: inventory = _mapping(raw_diagnostics.get("featureInventory")) object_counts = _normalized_count_dict(inventory.get("objectTypeCounts")) surface_counts = _normalized_count_dict(inventory.get("surfaceTypeCounts")) curve_counts = _normalized_count_dict(inventory.get("curveTypeCounts")) operation_counts = _normalized_count_dict(inventory.get("operationCounts")) scanned = _scan_raw_feature_counts(raw_objects) object_counts = _merge_count_dicts(object_counts, scanned["objectTypeCounts"]) surface_counts = _merge_count_dicts(surface_counts, scanned["surfaceTypeCounts"]) curve_counts = _merge_count_dicts(curve_counts, scanned["curveTypeCounts"]) operation_counts = _merge_count_dicts(operation_counts, scanned["operationCounts"]) edge_summary = _mapping(raw_diagnostics.get("edgeGeometrySummary")) edge_kind_counts = _normalized_count_dict(edge_summary.get("edgeKindCounts")) circular_edges = _count_value(edge_summary.get("circularEdgeCount")) or edge_kind_counts.get("circular", 0) linear_edges = edge_kind_counts.get("linear", 0) radius_buckets = edge_summary.get("circularRadiusBuckets") radius_bucket_count = len(radius_buckets) if isinstance(radius_buckets, list) else 0 cylinder_faces = surface_counts.get("cylinder", 0) planar_faces = surface_counts.get("plane", 0) round_objects = object_counts.get("round", 0) + object_counts.get("fillet", 0) slot_objects = object_counts.get("slot", 0) + object_counts.get("obround_slot", 0) + object_counts.get("rectangular_slot", 0) boss_objects = object_counts.get("boss", 0) + object_counts.get("cylindrical_boss", 0) + object_counts.get("rectangular_boss", 0) chamfer_objects = object_counts.get("chamfer", 0) pattern_objects = object_counts.get("pattern", 0) + object_counts.get("linear_pattern", 0) shell_objects = object_counts.get("shell", 0) + object_counts.get("thin_wall", 0) hints: list[dict[str, object]] = [] if slot_objects: _add_hint_group( hints, ("slot.width", "slot.depth", "slot.position"), slot_objects, "medium", "SCDM raw 已给出槽类对象;进入 S7 第二批真实命令和样例回测。", "scdm-object-type", ) elif cylinder_faces and circular_edges and linear_edges: _add_hint_group( hints, ("slot.width", "slot.depth", "slot.position"), min(cylinder_faces, circular_edges), "low", "当前模型含圆柱面、圆边和直边,具备槽/长圆孔分类材料;还需要确认具体槽组和编辑命令。", "scdm-geometry-summary", ) if boss_objects: _add_hint_group( hints, ("boss.height", "boss.diameter", "boss.position"), boss_objects, "medium", "SCDM raw 已给出凸台类对象;进入 S7 第三批真实命令和样例回测。", "scdm-object-type", ) elif cylinder_faces and planar_faces: _add_hint_group( hints, ("boss.height", "boss.diameter", "boss.position"), cylinder_faces, "low", "当前模型含圆柱面和平面,具备凸台/外圆分类材料;还需要区分孔、外圆、槽和圆角。", "scdm-geometry-summary", ) round_candidate_count = ( round_objects or operation_counts.get("change_round_radius", 0) or operation_counts.get("delete_round_or_chamfer", 0) ) if round_candidate_count: _add_hint_group( hints, ("round.radius", "feature.delete_round_or_chamfer"), round_candidate_count, "medium", "SCDM raw 已给出圆角/倒圆候选命令;执行器和专门质量校验仍待回测。", "scdm-command-candidate", ) elif circular_edges and radius_bucket_count: _add_hint_group( hints, ("round.radius",), circular_edges, "low", "当前模型含圆边和半径分组,具备圆角链分类材料;还需要确认支撑面和真实 Round 命令。", "scdm-edge-summary", ) if chamfer_objects: _add_hint_group( hints, ("chamfer.distance", "feature.delete_round_or_chamfer"), chamfer_objects, "medium", "SCDM raw 已给出倒角对象;进入 S7 第四批真实命令和样例回测。", "scdm-object-type", ) elif planar_faces and linear_edges and circular_edges: _add_hint_group( hints, ("chamfer.distance",), min(planar_faces, linear_edges), "low", "当前模型含大量平面和直边,具备倒角分类材料;还需要确认倒角斜面和相邻支撑面。", "scdm-geometry-summary", ) if pattern_objects: _add_hint_group( hints, ("pattern.spacing", "pattern.instance_position"), pattern_objects, "medium", "SCDM raw 已给出阵列对象;进入 S7 第五批真实命令和样例回测。", "scdm-object-type", ) elif radius_bucket_count and circular_edges >= 4: _add_hint_group( hints, ("pattern.spacing", "pattern.instance_position"), radius_bucket_count, "low", "当前模型含重复圆边半径分组,具备孔/圆角阵列识别材料;还需要确认实例分组和间距方向。", "scdm-edge-summary", ) if shell_objects: _add_hint_group( hints, ("shell.thickness",), shell_objects, "medium", "SCDM raw 已给出壳体/薄壁对象;进入 S7 第五批真实命令和样例回测。", "scdm-object-type", ) elif planar_faces >= 2: _add_hint_group( hints, ("shell.thickness",), planar_faces, "low", "当前模型含多组平面,具备薄壁/相对面厚度分类材料;还需要确认成对壁面和偏移方向。", "scdm-geometry-summary", ) return hints def _add_hint_group( hints: list[dict[str, object]], keys: tuple[str, ...], count: int, confidence: str, reason: str, source: str, ) -> None: evidence_count = max(0, int(count)) if evidence_count <= 0: return for key in keys: definition = capability_definition(key) if definition is None or definition.productized: continue hints.append( { "capabilityKey": key, "displayName": definition.display_name, "roadmapStage": definition.roadmap_stage, "evidenceCount": evidence_count, "confidence": confidence, "source": source, "reason": reason, } ) def _scan_raw_feature_counts(raw_objects: list[object]) -> dict[str, dict[str, int]]: result = { "objectTypeCounts": {}, "surfaceTypeCounts": {}, "curveTypeCounts": {}, "operationCounts": {}, } for raw_object in raw_objects: if not isinstance(raw_object, Mapping): continue _increment_count(result["objectTypeCounts"], raw_object.get("objectType")) geometry = _mapping(raw_object.get("geometry")) if geometry.get("surfaceType") is not None: _increment_count(result["surfaceTypeCounts"], geometry.get("surfaceType")) if geometry.get("curveType") is not None: _increment_count(result["curveTypeCounts"], geometry.get("curveType")) for command in raw_object.get("backendCommandCandidates", []) or []: if isinstance(command, Mapping) and command.get("enabled") is not False: _increment_count(result["operationCounts"], command.get("operation")) return result def _normalized_count_dict(value: object) -> dict[str, int]: if not isinstance(value, Mapping): return {} result: dict[str, int] = {} for key, count in value.items(): normalized = _count_key(key) number = _count_value(count) if normalized and number > 0: result[normalized] = result.get(normalized, 0) + number return result def _merge_count_dicts(primary: Mapping[str, int], fallback: Mapping[str, int]) -> dict[str, int]: result = {str(key): int(value) for key, value in primary.items() if int(value) > 0} for key, value in fallback.items(): text = str(key) number = int(value) if number > result.get(text, 0): result[text] = number return result def _increment_count(counts: dict[str, int], key: object) -> None: normalized = _count_key(key) if normalized: counts[normalized] = counts.get(normalized, 0) + 1 def _count_key(value: object) -> str: return str(value or "").strip().lower() def _count_value(value: object) -> int: try: return int(value) except (TypeError, ValueError): return 0 def map_scdm_raw_features_file(raw_path: str | Path, cache_path: str | Path | None = None) -> dict[str, object]: cache = map_scdm_raw_features(read_json(raw_path)) if cache_path is not None: write_json(cache_path, cache) return cache def _cylindrical_face_group_objects(raw_objects: list[object]) -> list[dict[str, object]]: groups: dict[tuple[object, ...], list[Mapping[str, object]]] = {} for raw_object in raw_objects: if not isinstance(raw_object, Mapping): continue geometry = _mapping(raw_object.get("geometry")) if _surface_key(geometry.get("surfaceType") or geometry.get("surface")) != "cylinder": continue center = _rounded_vector(geometry.get("center") or geometry.get("axisCenter")) axis = _canonical_axis(_rounded_vector(geometry.get("axis") or geometry.get("normal"))) radius = _rounded_number(geometry.get("radius")) if len(center) != 3 or len(axis) != 3 or radius is None or radius <= 0: continue topology = _mapping(raw_object.get("topologyHint")) body_index = _int_or_none(_first_present(topology.get("bodyIndex"), geometry.get("bodyIndex"))) key = (body_index, tuple(center), tuple(axis), radius) groups.setdefault(key, []).append(raw_object) result: list[dict[str, object]] = [] for (_body_index, center, axis, radius), members in groups.items(): if len(members) < 2: continue locators = [_locator_from_raw_object(member) for member in members] locators = [item for item in locators if item] topology = { "bodyIndex": _body_index, "faceOrdinal": _first_int(item.get("faceOrdinal") for item in locators), "faceOrdinals": [item["faceOrdinal"] for item in locators if item.get("faceOrdinal") is not None], "globalFaceOrdinal": _first_int(item.get("globalFaceOrdinal") for item in locators), "globalFaceOrdinals": [item["globalFaceOrdinal"] for item in locators if item.get("globalFaceOrdinal") is not None], "faceIds": _merge_ints(_mapping(member.get("topologyHint")).get("faceIds") for member in members), "scdmFaceLocators": locators, } source_ids = [str(member.get("backendId") or "") for member in members if str(member.get("backendId") or "")] result.append( { "backendId": "cylindrical_group:" + "|".join(source_ids), "objectType": "cylindrical_face_group", "geometry": { "surfaceType": "cylinder", "center": list(center), "axis": list(axis), "radius": radius, "diameter": radius * 2.0, "sourceBackendIds": source_ids, }, "topologyHint": topology, "backendCommandCandidates": [ {"operation": "change_hole_diameter", "enabled": True, "parameterFields": {"diameter": radius * 2.0}}, {"operation": "move_hole_axis", "enabled": True, "parameterFields": {"center": list(center)}}, {"operation": "fill_feature", "enabled": True, "parameterFields": {}}, ], "rawLimitations": [], } ) return result def _derived_feature_objects(source_objects: list[object]) -> list[dict[str, object]]: return [ *_derived_linear_pattern_objects(source_objects), *_derived_body_linear_pattern_objects(source_objects), *_derived_thin_wall_objects(source_objects), ] def _derived_linear_pattern_objects(source_objects: list[object]) -> list[dict[str, object]]: groups: dict[tuple[object, ...], list[dict[str, object]]] = {} for raw_object in source_objects: if not isinstance(raw_object, Mapping): continue object_type = str(raw_object.get("objectType") or "").strip().lower() if object_type not in {"hole", "cylindrical_hole", "cylindrical_face_group"}: continue geometry = _mapping(raw_object.get("geometry")) center = _rounded_vector(geometry.get("center") or geometry.get("axisCenter")) axis = _canonical_axis(_rounded_vector(geometry.get("axis") or geometry.get("normal"))) radius = _rounded_number(geometry.get("radius")) if radius is None: diameter = _rounded_number(geometry.get("diameter")) radius = diameter * 0.5 if diameter is not None else None if len(center) != 3 or len(axis) != 3 or radius is None or radius <= 0: continue topology = _mapping(raw_object.get("topologyHint")) locators = _face_locators(topology.get("scdmFaceLocators")) fallback_locator = _locator_from_raw_object(raw_object) if not locators and any(fallback_locator.get(key) is not None for key in ("bodyIndex", "faceOrdinal", "globalFaceOrdinal")): locators = [fallback_locator] key = ( _int_or_none(_first_present(topology.get("bodyIndex"), geometry.get("bodyIndex"))), tuple(axis), round(float(radius), 6), ) groups.setdefault(key, []).append( { "objectId": _object_id(raw_object), "center": center, "axis": axis, "radius": float(radius), "faceIds": _int_list(topology.get("faceIds") or geometry.get("faceIds")), "faceOrdinals": _int_list(topology.get("faceOrdinals") or [topology.get("faceOrdinal")]), "globalFaceOrdinals": _int_list( topology.get("globalFaceOrdinals") or [topology.get("globalFaceOrdinal"), topology.get("faceOrdinal")] ), "scdmFaceLocators": locators, } ) result: list[dict[str, object]] = [] seen: set[tuple[object, ...]] = set() for (body_index, _axis, radius), instances in groups.items(): if len(instances) < 3: continue candidates = _linear_pattern_candidates(instances) for candidate in candidates: member_ids = tuple(str(item.get("objectId") or "") for item in candidate["members"]) pattern_axis = tuple(round(float(value), 6) for value in candidate["direction"]) spacing = round(float(candidate["spacing"]), 6) key = (body_index, radius, pattern_axis, spacing, member_ids) if key in seen: continue seen.add(key) face_ids: list[int] = [] global_face_ordinals: list[int] = [] centers = [] pattern_instances: list[dict[str, object]] = [] for member in candidate["members"]: centers.append(list(member["center"])) face_ids.extend(_int_list(member.get("faceIds"))) global_face_ordinals.extend(_int_list(member.get("globalFaceOrdinals"))) pattern_instances.append( { "sourceObjectId": str(member.get("objectId") or ""), "center": list(member["center"]), "faceIds": _int_list(member.get("faceIds")), "faceOrdinals": _int_list(member.get("faceOrdinals")), "globalFaceOrdinals": _int_list(member.get("globalFaceOrdinals")), "scdmFaceLocators": _face_locators(member.get("scdmFaceLocators")), } ) center = [ round(sum(float(item[index]) for item in centers) / len(centers), 6) for index in range(3) ] result.append( { "backendId": "derived:linear_pattern:" + "|".join(member_ids), "objectType": "linear_pattern", "geometry": { "center": center, "axis": list(pattern_axis), "spacing": spacing, "pitch": spacing, "instanceCount": len(candidate["members"]), "instanceCenters": centers, "radius": radius, "diameter": radius * 2.0, "sourceObjectIds": list(member_ids), "patternInstances": pattern_instances, }, "topologyHint": { "bodyIndex": body_index, "faceIds": sorted(set(face_ids)), "globalFaceOrdinals": sorted(set(global_face_ordinals)), }, "backendCommandCandidates": [ {"operation": "change_pattern_spacing", "enabled": True, "parameterFields": {"spacing": spacing}}, ], "rawLimitations": [ "Derived from repeated cylindrical feature centers; spacing edit uses SCDM Move on each instance.", ], } ) if len(result) >= 24: return result return result def _linear_pattern_candidates(instances: list[dict[str, object]]) -> list[dict[str, object]]: if len(instances) < 3: return [] points = [list(item["center"]) for item in instances] diagonal = _point_cloud_diagonal(points) tolerance = max(diagonal * 1.0e-4, max(float(item.get("radius") or 0.0) for item in instances) * 0.02, 1.0e-5) candidates: list[dict[str, object]] = [] for left_index in range(len(instances)): for right_index in range(left_index + 1, len(instances)): p0 = points[left_index] p1 = points[right_index] direction = _unit_vector([p1[index] - p0[index] for index in range(3)]) if len(direction) != 3: continue collinear: list[tuple[float, dict[str, object]]] = [] for instance in instances: point = list(instance["center"]) delta = [point[index] - p0[index] for index in range(3)] projection = sum(delta[index] * direction[index] for index in range(3)) nearest = [p0[index] + projection * direction[index] for index in range(3)] distance = math.sqrt(sum((point[index] - nearest[index]) ** 2 for index in range(3))) if distance <= tolerance: collinear.append((projection, instance)) if len(collinear) < 3: continue collinear.sort(key=lambda item: item[0]) spacings = [ collinear[index + 1][0] - collinear[index][0] for index in range(len(collinear) - 1) if collinear[index + 1][0] - collinear[index][0] > tolerance ] if len(spacings) < 2: continue spacing = sum(spacings) / len(spacings) if spacing <= tolerance: continue spacing_tolerance = max(abs(spacing) * 0.03, tolerance * 3.0) if any(abs(value - spacing) > spacing_tolerance for value in spacings): continue members = [item[1] for item in collinear] canonical_direction = _canonical_axis([round(value, 6) for value in direction]) if not canonical_direction: continue member_ids = tuple(str(item.get("objectId") or "") for item in members) if any(set(member_ids) == set(str(member.get("objectId") or "") for member in existing["members"]) for existing in candidates): continue candidates.append({"members": members, "direction": canonical_direction, "spacing": spacing}) if len(candidates) >= 12: return candidates candidates.sort(key=lambda item: (-len(item["members"]), float(item["spacing"]))) return candidates def _derived_body_linear_pattern_objects(source_objects: list[object]) -> list[dict[str, object]]: body_summaries = _body_pattern_source_instances(source_objects) groups: dict[tuple[object, ...], list[dict[str, object]]] = {} for item in body_summaries: signature = tuple(item.get("shapeSignature") or ()) if not signature: continue groups.setdefault(signature, []).append(item) result: list[dict[str, object]] = [] seen: set[tuple[object, ...]] = set() for signature, instances in groups.items(): if len(instances) < 3: continue candidates = _linear_pattern_candidates(instances) for candidate in candidates: members = list(candidate["members"]) member_ids = tuple(str(item.get("objectId") or "") for item in members) pattern_axis = tuple(round(float(value), 6) for value in candidate["direction"]) spacing = round(float(candidate["spacing"]), 6) key = (signature, pattern_axis, spacing, member_ids) if key in seen: continue seen.add(key) centers = [list(item["center"]) for item in members] center = [ round(sum(float(item[index]) for item in centers) / len(centers), 6) for index in range(3) ] face_ids = sorted(set(value for item in members for value in _int_list(item.get("faceIds")))) face_ordinals = sorted(set(value for item in members for value in _int_list(item.get("faceOrdinals")))) global_face_ordinals = sorted(set(value for item in members for value in _int_list(item.get("globalFaceOrdinals")))) body_indices = [ value for value in (_int_or_none(item.get("bodyIndex")) for item in members) if value is not None ] pattern_instances = [ { "sourceObjectId": str(item.get("objectId") or ""), "instanceKind": "body", "center": list(item["center"]), "bodyIndex": _int_or_none(item.get("bodyIndex")), "bodyLocators": _body_locators(item.get("bodyLocators")), "componentLocators": _component_locators(item.get("componentLocators") or item.get("bodyLocators")), "faceIds": _int_list(item.get("faceIds")), "faceOrdinals": _int_list(item.get("faceOrdinals")), "globalFaceOrdinals": _int_list(item.get("globalFaceOrdinals")), "scdmFaceLocators": _face_locators(item.get("scdmFaceLocators")), } for item in members ] result.append( { "backendId": "derived:body_linear_pattern:" + "|".join(member_ids), "objectType": "linear_pattern", "geometry": { "patternKind": "body", "instanceKind": "body", "center": center, "axis": list(pattern_axis), "spacing": spacing, "pitch": spacing, "instanceCount": len(members), "instanceCenters": centers, "sourceObjectIds": list(member_ids), "bodyIndices": body_indices, "componentInstanceCount": sum( 1 for item in members if _component_locators(item.get("componentLocators") or item.get("bodyLocators")) ), "patternInstances": pattern_instances, }, "topologyHint": { "bodyIndices": body_indices, "faceIds": face_ids, "faceOrdinals": face_ordinals, "globalFaceOrdinals": global_face_ordinals, }, "backendCommandCandidates": [ {"operation": "change_pattern_spacing", "enabled": True, "parameterFields": {"spacing": spacing}}, ], "rawLimitations": [ "Derived from repeated SCDM Body/Part geometry centers; spacing edit opens only when every member has a component occurrence locator.", ], } ) if len(result) >= 24: return result return result def _body_pattern_source_instances(source_objects: list[object]) -> list[dict[str, object]]: explicit: list[dict[str, object]] = [] grouped_faces: dict[int, dict[str, object]] = {} for raw_object in source_objects: if not isinstance(raw_object, Mapping): continue object_type = str(raw_object.get("objectType") or "").strip().lower() geometry = _mapping(raw_object.get("geometry")) topology = _mapping(raw_object.get("topologyHint")) body_index = _int_or_none(_first_present(topology.get("bodyIndex"), geometry.get("bodyIndex"))) if object_type in {"body", "part", "component"} and body_index is not None: center = _rounded_vector(geometry.get("center") or geometry.get("bboxCenter") or geometry.get("axisCenter")) if len(center) == 3: explicit.append( { "objectId": _object_id(raw_object), "center": center, "bodyIndex": body_index, "bodyLocators": _body_locators(topology.get("bodyLocators") or geometry.get("bodyLocators") or [{"bodyIndex": body_index}]), "componentLocators": _component_locators(topology.get("componentLocators") or geometry.get("componentLocators")), "faceIds": _int_list(topology.get("faceIds") or geometry.get("faceIds")), "faceOrdinals": _int_list(topology.get("faceOrdinals") or geometry.get("faceOrdinals")), "globalFaceOrdinals": _int_list(topology.get("globalFaceOrdinals") or geometry.get("globalFaceOrdinals")), "scdmFaceLocators": _face_locators(topology.get("scdmFaceLocators") or geometry.get("scdmFaceLocators")), "shapeSignature": _body_shape_signature_from_object(raw_object), } ) continue if object_type != "face" or body_index is None: continue center = _rounded_vector(geometry.get("center") or geometry.get("axisCenter")) if len(center) != 3: continue item = grouped_faces.setdefault( int(body_index), { "objectId": f"body:{int(body_index)}", "bodyIndex": int(body_index), "centers": [], "faceIds": [], "faceOrdinals": [], "globalFaceOrdinals": [], "scdmFaceLocators": [], "bodyLocators": [], "componentLocators": [], "surfaceTypeCounts": {}, "radiusBuckets": {}, }, ) item["centers"].append(center) # type: ignore[index,union-attr] item["faceIds"].extend(_int_list(topology.get("faceIds") or geometry.get("faceIds"))) # type: ignore[index,union-attr] face_ordinal = _int_or_none(_first_present(topology.get("faceOrdinal"), geometry.get("faceOrdinal"))) if face_ordinal is not None: item["faceOrdinals"].append(face_ordinal) # type: ignore[index,union-attr] global_face_ordinal = _int_or_none(_first_present(topology.get("globalFaceOrdinal"), geometry.get("globalFaceOrdinal"))) if global_face_ordinal is not None: item["globalFaceOrdinals"].append(global_face_ordinal) # type: ignore[index,union-attr] locator = _locator_from_raw_object(raw_object) if any(locator.get(key) is not None for key in ("bodyIndex", "faceOrdinal", "globalFaceOrdinal")): item["scdmFaceLocators"].append(locator) # type: ignore[index,union-attr] item["bodyLocators"].extend(_body_locators(topology.get("bodyLocators") or geometry.get("bodyLocators"))) # type: ignore[index,union-attr] item["componentLocators"].extend(_component_locators(topology.get("componentLocators") or geometry.get("componentLocators"))) # type: ignore[index,union-attr] surface_key = _surface_key(geometry.get("surfaceType") or geometry.get("surface")) if surface_key: counts = item["surfaceTypeCounts"] # type: ignore[index] counts[surface_key] = counts.get(surface_key, 0) + 1 # type: ignore[union-attr] radius = _rounded_number(geometry.get("radius")) if radius is not None and radius > 0: buckets = item["radiusBuckets"] # type: ignore[index] bucket_key = round(float(radius), 6) buckets[bucket_key] = buckets.get(bucket_key, 0) + 1 # type: ignore[union-attr] if explicit: return [item for item in explicit if item.get("shapeSignature")] result: list[dict[str, object]] = [] for body_index, item in grouped_faces.items(): centers = [list(center) for center in item.get("centers", []) if isinstance(center, list) and len(center) == 3] locators = _face_locators(item.get("scdmFaceLocators")) if len(centers) < 2 or not locators: continue center = [ round(sum(float(point[index]) for point in centers) / len(centers), 6) for index in range(3) ] signature = _body_shape_signature_from_summary(item) if not signature: continue body_locators = _body_locators(item.get("bodyLocators")) or [{"bodyIndex": int(body_index)}] component_locators = _component_locators(item.get("componentLocators") or body_locators) result.append( { "objectId": str(item.get("objectId") or f"body:{body_index}"), "center": center, "bodyIndex": int(body_index), "bodyLocators": body_locators, "componentLocators": component_locators, "faceIds": sorted(set(_int_list(item.get("faceIds")))), "faceOrdinals": sorted(set(_int_list(item.get("faceOrdinals")))), "globalFaceOrdinals": sorted(set(_int_list(item.get("globalFaceOrdinals")))), "scdmFaceLocators": locators, "shapeSignature": signature, } ) return result def _body_shape_signature_from_object(raw_object: Mapping[str, object]) -> tuple[object, ...]: geometry = _mapping(raw_object.get("geometry")) bbox_size = _rounded_vector(geometry.get("bboxSize") or geometry.get("size") or geometry.get("boxSize")) face_count = _int_or_none(geometry.get("faceCount")) edge_count = _int_or_none(geometry.get("edgeCount")) signature: list[object] = [] if len(bbox_size) == 3: signature.append(("bbox", tuple(round(abs(float(value)), 5) for value in sorted(bbox_size)))) if face_count is not None: signature.append(("faces", face_count)) if edge_count is not None: signature.append(("edges", edge_count)) return tuple(signature) def _body_shape_signature_from_summary(summary: Mapping[str, object]) -> tuple[object, ...]: face_count = len(_int_list(summary.get("faceOrdinals"))) or len(_face_locators(summary.get("scdmFaceLocators"))) if face_count < 2: return () surface_counts = _mapping(summary.get("surfaceTypeCounts")) radius_buckets = _mapping(summary.get("radiusBuckets")) return ( ("faces", face_count), ("surfaces", tuple(sorted((str(key), int(value)) for key, value in surface_counts.items()))), ("radii", tuple(sorted((float(key), int(value)) for key, value in radius_buckets.items()))), ) def _derived_thin_wall_objects(source_objects: list[object]) -> list[dict[str, object]]: groups: dict[tuple[object, ...], list[dict[str, object]]] = {} for raw_object in source_objects: if not isinstance(raw_object, Mapping): continue if str(raw_object.get("objectType") or "").strip().lower() != "face": continue geometry = _mapping(raw_object.get("geometry")) if _surface_key(geometry.get("surfaceType") or geometry.get("surface")) != "plane": continue center = _rounded_vector(geometry.get("center") or geometry.get("axisCenter")) normal = _unit_vector(_rounded_vector(geometry.get("normal") or geometry.get("axis"))) axis = _canonical_axis(normal) if len(center) != 3 or len(axis) != 3: continue locator = _locator_from_raw_object(raw_object) if not any(locator.get(key) is not None for key in ("bodyIndex", "faceOrdinal", "globalFaceOrdinal")): continue topology = _mapping(raw_object.get("topologyHint")) body_index = _int_or_none(_first_present(topology.get("bodyIndex"), geometry.get("bodyIndex"))) key = (body_index, tuple(axis)) groups.setdefault(key, []).append( { "objectId": _object_id(raw_object), "center": center, "axis": axis, "faceIds": _int_list(topology.get("faceIds") or geometry.get("faceIds")), "faceOrdinal": _int_or_none(_first_present(topology.get("faceOrdinal"), geometry.get("faceOrdinal"))), "globalFaceOrdinal": _int_or_none(_first_present(topology.get("globalFaceOrdinal"), geometry.get("globalFaceOrdinal"))), "scdmFaceLocators": [locator], } ) result: list[dict[str, object]] = [] seen: set[tuple[object, ...]] = set() for (body_index, axis_key), faces in groups.items(): if len(faces) < 2: continue candidates = _thin_wall_pair_candidates(faces, list(axis_key)) for candidate in candidates: left = candidate["left"] right = candidate["right"] source_ids = (str(left.get("objectId") or ""), str(right.get("objectId") or "")) ordinals = tuple( value for value in ( _int_or_none(left.get("faceOrdinal")), _int_or_none(right.get("faceOrdinal")), ) if value is not None ) global_ordinals = tuple( value for value in ( _int_or_none(left.get("globalFaceOrdinal")), _int_or_none(right.get("globalFaceOrdinal")), ) if value is not None ) key = (body_index, tuple(axis_key), round(float(candidate["thickness"]), 6), tuple(sorted(source_ids))) if key in seen: continue seen.add(key) locators = [ locator for item in (left, right) for locator in _face_locators(item.get("scdmFaceLocators")) ] centers = [list(left["center"]), list(right["center"])] center = [ round((float(centers[0][index]) + float(centers[1][index])) * 0.5, 6) for index in range(3) ] result.append( { "backendId": "derived:thin_wall:" + "|".join(source_ids), "objectType": "thin_wall", "geometry": { "surfaceType": "plane", "center": center, "axis": list(axis_key), "thicknessAxis": list(axis_key), "thickness": round(float(candidate["thickness"]), 6), "wallFaceCenters": centers, "sourceObjectIds": list(source_ids), "wallFaceLocators": locators, }, "topologyHint": { "bodyIndex": body_index, "faceIds": sorted(set(_int_list(left.get("faceIds")) + _int_list(right.get("faceIds")))), "faceOrdinals": list(ordinals), "globalFaceOrdinals": list(global_ordinals), "scdmFaceLocators": locators, "wallFaceLocators": locators, }, "backendCommandCandidates": [ {"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.", ], } ) if len(result) >= 24: return result return result def _thin_wall_pair_candidates(faces: list[dict[str, object]], axis: list[float]) -> list[dict[str, object]]: pairs: list[dict[str, object]] = [] for left_index in range(len(faces)): for right_index in range(left_index + 1, len(faces)): left = faces[left_index] right = faces[right_index] delta = [ float(right["center"][index]) - float(left["center"][index]) for index in range(3) ] along = sum(delta[index] * axis[index] for index in range(3)) thickness = abs(along) if thickness <= 1.0e-9: continue delta_length_sq = sum(value * value for value in delta) perpendicular = math.sqrt(max(delta_length_sq - along * along, 0.0)) if perpendicular > max(thickness * 0.25, 1.0e-4): continue pairs.append({"left": left, "right": right, "thickness": thickness, "perpendicular": perpendicular}) if not pairs: return [] min_thickness = min(float(item["thickness"]) for item in pairs) tolerance = max(min_thickness * 0.05, 1.0e-6) filtered = [item for item in pairs if abs(float(item["thickness"]) - min_thickness) <= tolerance] filtered.sort(key=lambda item: (float(item["thickness"]), float(item["perpendicular"]))) return filtered[:12] def _point_cloud_diagonal(points: list[list[float]]) -> float: if not points: return 0.0 mins = [min(float(point[index]) for point in points) for index in range(3)] maxs = [max(float(point[index]) for point in points) for index in range(3)] return math.sqrt(sum((maxs[index] - mins[index]) ** 2 for index in range(3))) def _unit_vector(values: list[float]) -> list[float]: length = math.sqrt(sum(float(item) * float(item) for item in values)) if length <= 1.0e-12: return [] return [float(item) / length for item in values] def geometry_signature(raw_object: Mapping[str, object]) -> dict[str, object]: geometry = _mapping(raw_object.get("geometry")) topology = _mapping(raw_object.get("topologyHint")) round_info = _mapping(geometry.get("roundInfo")) chamfer_info = _mapping(geometry.get("chamferInfo")) slot_info = _mapping(geometry.get("slotInfo")) return { "objectType": str(raw_object.get("objectType") or ""), "patternKind": str(geometry.get("patternKind") or topology.get("patternKind") or ""), "instanceKind": str(geometry.get("instanceKind") or topology.get("instanceKind") or ""), "faceIds": _int_list(topology.get("faceIds") or geometry.get("faceIds")), "edgeIds": _int_list(topology.get("edgeIds") or geometry.get("edgeIds")), "bodyIndex": _int_or_none(_first_present(topology.get("bodyIndex"), geometry.get("bodyIndex"))), "bodyIndices": _int_list(topology.get("bodyIndices") or geometry.get("bodyIndices")), "faceOrdinal": _int_or_none(_first_present(topology.get("faceOrdinal"), geometry.get("faceOrdinal"))), "faceOrdinals": _int_list(topology.get("faceOrdinals") or geometry.get("faceOrdinals")), "edgeOrdinal": _int_or_none(_first_present(topology.get("edgeOrdinal"), geometry.get("edgeOrdinal"))), "globalFaceOrdinal": _int_or_none(_first_present(topology.get("globalFaceOrdinal"), geometry.get("globalFaceOrdinal"))), "globalFaceOrdinals": _int_list(topology.get("globalFaceOrdinals") or geometry.get("globalFaceOrdinals")), "globalEdgeOrdinal": _int_or_none(_first_present(topology.get("globalEdgeOrdinal"), geometry.get("globalEdgeOrdinal"))), "adjacentFaceOrdinals": _int_list(topology.get("adjacentFaceOrdinals") or geometry.get("adjacentFaceOrdinals")), "adjacentFaceCount": _int_or_none(_first_present(topology.get("adjacentFaceCount"), geometry.get("adjacentFaceCount"))), "heightFaceOrdinals": _int_list( topology.get("heightFaceOrdinals") or topology.get("topFaceOrdinals") or geometry.get("heightFaceOrdinals") or geometry.get("topFaceOrdinals") ), "globalHeightFaceOrdinals": _int_list( topology.get("globalHeightFaceOrdinals") or topology.get("globalTopFaceOrdinals") or geometry.get("globalHeightFaceOrdinals") or geometry.get("globalTopFaceOrdinals") ), "depthFaceOrdinals": _int_list( topology.get("depthFaceOrdinals") or topology.get("bottomFaceOrdinals") or geometry.get("depthFaceOrdinals") or geometry.get("bottomFaceOrdinals") ), "globalDepthFaceOrdinals": _int_list( topology.get("globalDepthFaceOrdinals") or topology.get("globalBottomFaceOrdinals") or geometry.get("globalDepthFaceOrdinals") or geometry.get("globalBottomFaceOrdinals") ), "diameterFaceOrdinals": _int_list( topology.get("diameterFaceOrdinals") or topology.get("sideFaceOrdinals") or geometry.get("diameterFaceOrdinals") or geometry.get("sideFaceOrdinals") ), "globalDiameterFaceOrdinals": _int_list( topology.get("globalDiameterFaceOrdinals") or topology.get("globalSideFaceOrdinals") or geometry.get("globalDiameterFaceOrdinals") or geometry.get("globalSideFaceOrdinals") ), "surfaceType": str(geometry.get("surfaceType") or geometry.get("surface") or ""), "curveType": str(geometry.get("curveType") or ""), "center": _rounded_vector(geometry.get("center") or geometry.get("axisCenter")), "axis": _rounded_vector(geometry.get("axis") or geometry.get("normal")), "startPoint": _rounded_vector(geometry.get("startPoint")), "endPoint": _rounded_vector(geometry.get("endPoint")), "midPoint": _rounded_vector(geometry.get("midPoint")), "length": _rounded_number(geometry.get("length")), "width": _rounded_number(geometry.get("width")), "depth": _rounded_number(_first_present(slot_info.get("depth"), geometry.get("depth"))), "depthAxis": _rounded_vector( _first_present( slot_info.get("depthAxis"), slot_info.get("depthDirection"), geometry.get("depthAxis"), geometry.get("depthDirection"), ) ), "height": _rounded_number(geometry.get("height")), "thickness": _rounded_number(geometry.get("thickness")), "thicknessAxis": _rounded_vector(geometry.get("thicknessAxis") or geometry.get("thicknessDirection")), "distance": _rounded_number(_first_present(chamfer_info.get("distance"), geometry.get("distance"), geometry.get("offset"))), "distance1": _rounded_number(_first_present(chamfer_info.get("distance1"), geometry.get("distance1"))), "distance2": _rounded_number(_first_present(chamfer_info.get("distance2"), geometry.get("distance2"))), "radius": _rounded_number(_first_present(round_info.get("radius"), geometry.get("radius"))), "diameter": _rounded_number(_first_present(round_info.get("diameter"), geometry.get("diameter"))), "roundType": str(round_info.get("type") or geometry.get("roundType") or ""), "isConstantRound": _bool_or_none(_first_present(round_info.get("isConstant"), geometry.get("isConstant"), geometry.get("constant"))), "isRound": _bool_or_none(_first_present(round_info.get("isRound"), geometry.get("isRound"))), "chamferType": str(chamfer_info.get("type") or geometry.get("chamferType") or ""), "isEqualDistanceChamfer": _bool_or_none( _first_present( chamfer_info.get("isEqualDistance"), chamfer_info.get("isSymmetric"), geometry.get("isEqualDistanceChamfer"), geometry.get("isEqualDistance"), ) ), "spacing": _rounded_number(geometry.get("spacing")), "pitch": _rounded_number(geometry.get("pitch")), "instanceCount": _int_or_none(geometry.get("instanceCount")), "instanceCenters": _rounded_vector_list(geometry.get("instanceCenters")), "patternInstances": _pattern_instances(geometry.get("patternInstances") or topology.get("patternInstances")), "sourceObjectIds": _string_list(geometry.get("sourceObjectIds")), "bodyLocators": _body_locators(topology.get("bodyLocators") or geometry.get("bodyLocators")), "componentLocators": _component_locators(topology.get("componentLocators") or geometry.get("componentLocators")), "planeOffset": _rounded_number(geometry.get("planeOffset")), "scdmFaceLocators": _face_locators(topology.get("scdmFaceLocators")), "heightFaceLocators": _face_locators( topology.get("heightFaceLocators") or topology.get("topFaceLocators") or geometry.get("heightFaceLocators") or geometry.get("topFaceLocators") ), "depthFaceLocators": _face_locators( topology.get("depthFaceLocators") or topology.get("bottomFaceLocators") or geometry.get("depthFaceLocators") or geometry.get("bottomFaceLocators") ), "diameterFaceLocators": _face_locators( topology.get("diameterFaceLocators") or topology.get("sideFaceLocators") or geometry.get("diameterFaceLocators") or geometry.get("sideFaceLocators") ), "wallFaceLocators": _face_locators( topology.get("wallFaceLocators") or geometry.get("wallFaceLocators") ), } def attach_local_face_ids_to_scdm_cache( cache: Mapping[str, object], local_face_signatures: Iterable[Mapping[str, object]], *, min_score: float = 5.0, unique_margin: float = 0.75, ) -> dict[str, object]: local_signatures = [dict(item) for item in local_face_signatures if isinstance(item, Mapping)] result = dict(cache) diagnostics = dict(result.get("diagnostics") if isinstance(result.get("diagnostics"), Mapping) else {}) mapping_rows: list[dict[str, object]] = [] objects = [] for raw_object in result.get("objects", []) if isinstance(result.get("objects"), list) else []: if not isinstance(raw_object, Mapping): continue item = dict(raw_object) signature = dict(item.get("geometrySignature") if isinstance(item.get("geometrySignature"), Mapping) else {}) if not _int_list(signature.get("faceIds")): ordinal_face_ids = _face_ids_from_signature_ordinals(signature, local_signatures) if ordinal_face_ids: signature["faceIds"] = ordinal_face_ids signature["localOrdinalMatch"] = True match = { "status": "ordinal", "message": "Matched local Face IDs from SCDM ordinal locators.", "faceIds": ordinal_face_ids, "score": None, } else: if str(signature.get("objectType") or "") in {"cylindrical_face_group", "cylindrical_hole"}: match = _match_local_face_group_signatures(signature, local_signatures, min_score=min_score, unique_margin=unique_margin) else: match = _match_local_face_signature(signature, local_signatures, min_score=min_score, unique_margin=unique_margin) status = str(match.get("status") or "") if status in {"unique", "group"}: face_ids = _int_list(match.get("faceIds")) face_id = _int_or_none(match.get("faceId")) if not face_ids and face_id is not None: face_ids = [face_id] if face_ids: signature["faceIds"] = face_ids signature["localMatchScore"] = match.get("score") signature["localUnitScale"] = match.get("unitScale") status = str(match.get("status") or "") mapping_rows.append( { "objectId": item.get("objectId"), "status": status, "faceId": match.get("faceId"), "faceIds": match.get("faceIds"), "score": match.get("score"), "message": match.get("message"), } ) unit_scale = _inferred_local_unit_scale(signature, local_signatures) if unit_scale is not None: signature["localUnitScale"] = unit_scale support_face_ids = _pattern_support_face_ids(signature, local_signatures) if support_face_ids: signature["supportFaceIds"] = support_face_ids support_fit = _pattern_support_fit(signature, local_signatures, support_face_ids, unit_scale or 1.0) if support_fit: signature["supportPatternFit"] = support_fit _attach_pattern_instance_local_display_ids(signature, local_signatures) item["geometrySignature"] = signature objects.append(item) diagnostics["local_face_mapping"] = mapping_rows result["objects"] = objects result["diagnostics"] = diagnostics return result def _face_ids_from_signature_ordinals( signature: Mapping[str, object], local_signatures: list[dict[str, object]], ) -> list[int]: global_lookup: dict[int, int] = {} body_lookup: dict[tuple[int, int], int] = {} for local in local_signatures: face_id = _int_or_none(local.get("faceId")) if face_id is None: continue global_ordinal = _int_or_none(local.get("globalFaceOrdinal")) if global_ordinal is not None: global_lookup[global_ordinal] = face_id body_index = _int_or_none(_first_present(local.get("bodyIndex"), local.get("solidId"))) face_ordinal = _int_or_none(local.get("faceOrdinal")) if body_index is not None and face_ordinal is not None: body_lookup[(body_index, face_ordinal)] = face_id result: list[int] = [] for value in _int_list(signature.get("globalFaceOrdinals") or [signature.get("globalFaceOrdinal")]): face_id = global_lookup.get(value) if face_id is not None: result.append(face_id) body_indices = _int_list(signature.get("bodyIndices")) if not body_indices: body_index = _int_or_none(signature.get("bodyIndex")) body_indices = [body_index] if body_index is not None else [] for body_index in body_indices: for face_ordinal in _int_list(signature.get("faceOrdinals") or [signature.get("faceOrdinal")]): face_id = body_lookup.get((int(body_index), int(face_ordinal))) if face_id is not None: result.append(face_id) for locator in _face_locators(signature.get("scdmFaceLocators")): face_id = None global_ordinal = _int_or_none(locator.get("globalFaceOrdinal")) if global_ordinal is not None: face_id = global_lookup.get(global_ordinal) if face_id is None: body_index = _int_or_none(locator.get("bodyIndex")) face_ordinal = _int_or_none(locator.get("faceOrdinal")) if body_index is not None and face_ordinal is not None: face_id = body_lookup.get((body_index, face_ordinal)) if face_id is not None: result.append(face_id) return sorted(set(result)) def _attach_pattern_instance_local_display_ids( signature: dict[str, object], local_signatures: list[dict[str, object]], ) -> None: instances = signature.get("patternInstances") if not isinstance(instances, list): return local_by_face_id = { int(face_id): local for local in local_signatures for face_id in [_int_or_none(local.get("faceId"))] if face_id is not None } updated_instances: list[object] = [] for instance in instances: if not isinstance(instance, Mapping): updated_instances.append(instance) continue row = dict(instance) face_ids = _int_list(row.get("faceIds")) if not face_ids: face_ids = _face_ids_from_signature_ordinals(row, local_signatures) if face_ids: row["faceIds"] = face_ids solid_ids = sorted( set( int(value) for face_id in face_ids for value in [ _int_or_none( _first_present( local_by_face_id.get(int(face_id), {}).get("solidId"), local_by_face_id.get(int(face_id), {}).get("bodyIndex"), ) ) ] if value is not None and int(value) >= 0 ) ) part_ids = sorted( set( int(value) for face_id in face_ids for value in [_int_or_none(local_by_face_id.get(int(face_id), {}).get("partId"))] if value is not None and int(value) >= 0 ) ) if solid_ids: row["localSolidIds"] = solid_ids if len(solid_ids) == 1: row["localSolidId"] = solid_ids[0] if part_ids: row["localPartIds"] = part_ids if len(part_ids) == 1: row["localPartId"] = part_ids[0] updated_instances.append(row) signature["patternInstances"] = updated_instances def _inferred_local_unit_scale( signature: Mapping[str, object], local_signatures: list[dict[str, object]], ) -> float | None: existing = _number(signature.get("localUnitScale")) if existing is not None and existing > 0: return existing if str(signature.get("patternKind") or "").lower() == "body": scale = _body_pattern_unit_scale(signature, local_signatures) if scale is not None and scale > 0: return scale by_face_id = { int(face_id): local for local in local_signatures for face_id in [_int_or_none(local.get("faceId"))] if face_id is not None } scored: list[tuple[float, float]] = [] for face_id in _int_list(signature.get("faceIds"))[:32]: local = by_face_id.get(int(face_id)) if local is None: continue score, scale = _local_signature_score(signature, local) if score > 0 and scale > 0: scored.append((score, scale)) if scored: scored.sort(key=lambda item: item[0], reverse=True) return scored[0][1] return None def _body_pattern_unit_scale( signature: Mapping[str, object], local_signatures: list[dict[str, object]], ) -> float | None: if str(signature.get("patternKind") or "").lower() != "body": return None axis = _unit_vector(_rounded_vector(signature.get("axis"))) if len(axis) != 3: return None scdm_spacing = _rounded_number(_first_present(signature.get("spacing"), signature.get("pitch"))) if scdm_spacing is None or scdm_spacing <= 0: return None body_boxes = _local_body_boxes(local_signatures) body_indices = _int_list(signature.get("bodyIndices")) centers = [_box_center(body_boxes[index]) for index in body_indices if index in body_boxes] if len(centers) < 2: return None projections = sorted(sum(center[index] * axis[index] for index in range(3)) for center in centers) spacings = [ projections[index + 1] - projections[index] for index in range(len(projections) - 1) if projections[index + 1] - projections[index] > 1.0e-9 ] if not spacings: return None local_spacing = sum(spacings) / len(spacings) if local_spacing <= 0: return None return float(scdm_spacing) / float(local_spacing) def _pattern_support_face_ids( signature: Mapping[str, object], local_signatures: list[dict[str, object]], ) -> list[int]: if str(signature.get("patternKind") or "").lower() != "body": return [] body_indices = _int_list(signature.get("bodyIndices")) if len(body_indices) < 3: return [] body_boxes = _local_body_boxes(local_signatures) member_boxes = [body_boxes[index] for index in body_indices if index in body_boxes] if len(member_boxes) < 3: return [] pattern_box = _merge_boxes(member_boxes) pattern_size = [pattern_box[1][index] - pattern_box[0][index] for index in range(3)] diagonal = math.sqrt(sum(item * item for item in pattern_size)) tolerance = max(diagonal * 1.0e-5, 1.0e-5) pattern_body_set = set(body_indices) candidates: list[tuple[float, int]] = [] for local in local_signatures: face_id = _int_or_none(local.get("faceId")) body_index = _int_or_none(_first_present(local.get("bodyIndex"), local.get("solidId"))) if face_id is None or body_index in pattern_body_set: continue if _surface_key(local.get("surfaceType")) != "plane": continue axis = _unit_vector(_rounded_vector(local.get("axis"))) if len(axis) != 3: continue face_box = _box_from_local_signature(local) if face_box is None: continue plane = _rounded_number(local.get("planeOffset")) if plane is None: continue overlap = _pattern_projection_overlap_score(pattern_box, face_box, axis) if overlap <= 0: continue distance = _pattern_plane_touch_distance(pattern_box, axis, plane) 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]] def _pattern_support_fit( signature: Mapping[str, object], local_signatures: list[dict[str, object]], support_face_ids: list[int], unit_scale: float, ) -> dict[str, object]: if not support_face_ids: return {} axis = _unit_vector(_rounded_vector(signature.get("axis"))) if len(axis) != 3: return {} body_boxes = _local_body_boxes(local_signatures) member_boxes = [body_boxes[index] for index in _int_list(signature.get("bodyIndices")) if index in body_boxes] if len(member_boxes) < 3: return {} member_centers = [_box_center(box) for box in member_boxes] center_projections = sorted(_point_projection(center, axis) for center in member_centers) if len(center_projections) < 3: return {} member_span = max(_box_projection_span(box, axis) for box in member_boxes) if member_span <= 0: return {} local_by_face_id = { int(face_id): local for local in local_signatures for face_id in [_int_or_none(local.get("faceId"))] if face_id is not None } support_candidates = [] for face_id in support_face_ids: local = local_by_face_id.get(int(face_id)) if local is None: continue box = _box_from_local_signature(local) if box is None: continue projection_min, projection_max = _box_projection_range(box, axis) pattern_center_projection = sum(center_projections) / len(center_projections) half_member_span = member_span * 0.5 left_capacity = pattern_center_projection - projection_min - half_member_span right_capacity = projection_max - pattern_center_projection - half_member_span max_spacing_local = (2.0 * min(left_capacity, right_capacity)) / (len(center_projections) - 1) if max_spacing_local <= 0: continue support_candidates.append( { "faceId": int(face_id), "supportSpanLocal": projection_max - projection_min, "supportProjectionMinLocal": projection_min, "supportProjectionMaxLocal": projection_max, "memberSpanLocal": member_span, "patternCenterProjectionLocal": pattern_center_projection, "leftCapacityLocal": left_capacity, "rightCapacityLocal": right_capacity, "maxSpacingLocal": max_spacing_local, } ) if not support_candidates: return {} support_candidates.sort(key=lambda item: float(item["maxSpacingLocal"]), reverse=True) selected = support_candidates[0] safe_unit_scale = unit_scale if unit_scale > 0 else 1.0 max_spacing_local = float(selected["maxSpacingLocal"]) current_spacing = _rounded_number(_first_present(signature.get("spacing"), signature.get("pitch"))) selected.update( { "supportFaceIds": support_face_ids, "instanceCount": len(center_projections), "axis": axis, "localUnitScale": safe_unit_scale, "maxSpacing": max_spacing_local * safe_unit_scale, "currentSpacing": current_spacing, "currentSpacingLocal": (current_spacing / safe_unit_scale) if current_spacing is not None and safe_unit_scale > 0 else None, } ) return selected def _local_body_boxes(local_signatures: list[dict[str, object]]) -> dict[int, tuple[list[float], list[float]]]: boxes: dict[int, tuple[list[float], list[float]]] = {} for local in local_signatures: body_index = _int_or_none(_first_present(local.get("bodyIndex"), local.get("solidId"))) box = _box_from_local_signature(local) if body_index is None or box is None: continue if body_index not in boxes: boxes[body_index] = ([*box[0]], [*box[1]]) continue existing = boxes[body_index] boxes[body_index] = ( [min(existing[0][index], box[0][index]) for index in range(3)], [max(existing[1][index], box[1][index]) for index in range(3)], ) return boxes def _box_from_local_signature(local: Mapping[str, object]) -> tuple[list[float], list[float]] | None: bbox_min = _rounded_vector(local.get("bboxMin")) bbox_max = _rounded_vector(local.get("bboxMax")) if len(bbox_min) != 3 or len(bbox_max) != 3: return None return bbox_min, bbox_max def _merge_boxes(boxes: list[tuple[list[float], list[float]]]) -> tuple[list[float], list[float]]: return ( [min(box[0][index] for box in boxes) for index in range(3)], [max(box[1][index] for box in boxes) for index in range(3)], ) def _box_center(box: tuple[list[float], list[float]]) -> list[float]: return [(box[0][index] + box[1][index]) * 0.5 for index in range(3)] def _point_projection(point: list[float], axis: list[float]) -> float: return sum(float(point[index]) * float(axis[index]) for index in range(3)) def _box_projection_range(box: tuple[list[float], list[float]], axis: list[float]) -> tuple[float, float]: values = [] for x in (box[0][0], box[1][0]): for y in (box[0][1], box[1][1]): for z in (box[0][2], box[1][2]): values.append(_point_projection([x, y, z], axis)) return (min(values), max(values)) if values else (0.0, 0.0) def _box_projection_span(box: tuple[list[float], list[float]], axis: list[float]) -> float: left, right = _box_projection_range(box, axis) return max(0.0, right - left) def _pattern_projection_overlap_score( pattern_box: tuple[list[float], list[float]], face_box: tuple[list[float], list[float]], normal: list[float], ) -> float: normal_axis = max(range(3), key=lambda index: abs(normal[index])) overlaps = [] for axis_index in range(3): if axis_index == normal_axis: continue overlap = min(pattern_box[1][axis_index], face_box[1][axis_index]) - max(pattern_box[0][axis_index], face_box[0][axis_index]) reference = max(pattern_box[1][axis_index] - pattern_box[0][axis_index], 1.0e-9) overlaps.append(max(0.0, overlap) / reference) if not overlaps or any(value <= 0 for value in overlaps): return 0.0 return sum(overlaps) / len(overlaps) def _pattern_plane_touch_distance( pattern_box: tuple[list[float], list[float]], normal: list[float], plane_offset: float, ) -> float: distances = [] for x in (pattern_box[0][0], pattern_box[1][0]): for y in (pattern_box[0][1], pattern_box[1][1]): for z in (pattern_box[0][2], pattern_box[1][2]): distances.append(abs(x * normal[0] + y * normal[1] + z * normal[2] - plane_offset)) return min(distances) if distances else float("inf") def _box_area_estimate(box: tuple[list[float], list[float]]) -> float: sizes = sorted(max(0.0, box[1][index] - box[0][index]) for index in range(3)) return sizes[1] * sizes[2] def _match_local_face_signature( scdm_signature: Mapping[str, object], local_signatures: list[dict[str, object]], *, min_score: float, unique_margin: float, ) -> dict[str, object]: scored = [] for local in local_signatures: score, scale = _local_signature_score(scdm_signature, local) if score <= 0: continue scored.append({"faceId": local.get("faceId"), "score": score, "unitScale": scale}) scored.sort(key=lambda item: float(item.get("score") or 0.0), reverse=True) if not scored or float(scored[0].get("score") or 0.0) < min_score: return {"status": "none", "message": "No local Face signature matched SCDM object.", "candidates": scored[:5]} if len(scored) > 1: top = float(scored[0].get("score") or 0.0) second = float(scored[1].get("score") or 0.0) if top - second < unique_margin: return {"status": "multiple", "message": "Multiple local Faces match the same SCDM object.", "candidates": scored[:5]} return {"status": "unique", "message": "Matched one local Face.", **scored[0]} def _match_local_face_group_signatures( scdm_signature: Mapping[str, object], local_signatures: list[dict[str, object]], *, min_score: float, unique_margin: float, ) -> dict[str, object]: scored = [] for local in local_signatures: score, scale = _local_signature_score(scdm_signature, local) if score < min_score: continue scored.append({"faceId": local.get("faceId"), "score": score, "unitScale": scale}) scored.sort(key=lambda item: float(item.get("score") or 0.0), reverse=True) if not scored: return {"status": "none", "message": "No local cylindrical Face matched SCDM group.", "candidates": []} top = float(scored[0].get("score") or 0.0) selected = [item for item in scored if top - float(item.get("score") or 0.0) <= unique_margin] face_ids = sorted(set(_int_or_none(item.get("faceId")) for item in selected if _int_or_none(item.get("faceId")) is not None)) if not face_ids: return {"status": "none", "message": "Local cylindrical group matched no usable Face IDs.", "candidates": scored[:5]} return { "status": "group", "message": "Matched a local cylindrical Face group.", "faceIds": face_ids, "score": top, "unitScale": selected[0].get("unitScale"), "candidates": scored[:8], } def _local_signature_score(scdm_signature: Mapping[str, object], local: Mapping[str, object]) -> tuple[float, float]: scdm_surface = _surface_key(scdm_signature.get("surfaceType")) local_surface = _surface_key(local.get("surfaceType")) if scdm_surface and local_surface and scdm_surface != local_surface: return 0.0, 1.0 scales = _unit_scale_candidates(scdm_signature, local) best_score = 0.0 best_scale = 1.0 for scale in scales: score = 2.0 if scdm_surface and scdm_surface == local_surface else 0.0 score += _axis_match_score(_rounded_vector(scdm_signature.get("axis")), _rounded_vector(local.get("axis"))) if scdm_surface == "plane": score += _scaled_number_match_score( _rounded_number(local.get("planeOffset")), _rounded_number(scdm_signature.get("planeOffset")), scale, ) elif scdm_surface == "cylinder": score += _scaled_number_match_score( _rounded_number(local.get("radius")), _rounded_number(scdm_signature.get("radius")), scale, ) score += _axis_point_distance_score( _rounded_vector(local.get("center")), _rounded_vector(local.get("axis")), _rounded_vector(scdm_signature.get("center")), scale, ) else: score += _scaled_vector_match_score( _rounded_vector(local.get("center")), _rounded_vector(scdm_signature.get("center")), scale, ) if score > best_score: best_score = score best_scale = scale return best_score, best_scale def _unit_scale_candidates(scdm_signature: Mapping[str, object], local: Mapping[str, object]) -> tuple[float, ...]: values = [1.0, 0.001, 1000.0] local_radius = _rounded_number(local.get("radius")) scdm_radius = _rounded_number(scdm_signature.get("radius")) if local_radius and scdm_radius: values.append(float(scdm_radius) / float(local_radius)) local_offset = _rounded_number(local.get("planeOffset")) scdm_offset = _rounded_number(scdm_signature.get("planeOffset")) if local_offset not in {None, 0.0} and scdm_offset is not None: values.append(float(scdm_offset) / float(local_offset)) result = [] for value in values: if value > 0 and all(abs(value - existing) > max(value, existing) * 1e-9 for existing in result): result.append(value) return tuple(result) def _capability_payload( key: str, raw_object: Mapping[str, object], *, available_command_names: set[str] | None = None, ) -> dict[str, object] | None: definition = capability_definition(key) if definition is None: return None block_reason = _first_non_empty( _missing_backend_command_reason(key, available_command_names), _missing_geometry_reason(key, raw_object), ) return { "key": definition.key, "displayName": definition.display_name, "valueKind": definition.value_kind, "currentValue": _current_value(definition.current_fields, raw_object), "defaultIntent": definition.default_intent, "backendOperation": definition.backend_operation, "postCheck": definition.post_check, "editable": True, "blockReason": block_reason, } def _current_value(fields: tuple[str, ...], raw_object: Mapping[str, object]) -> object: geometry = _mapping(raw_object.get("geometry")) for field in fields: if field == "0": return 0.0 if field == "1": return 1 if field == "geometry.radius*2": radius = _number(geometry.get("radius")) if radius is not None: return radius * 2.0 continue if field.startswith("geometry."): value = _path_value(geometry, field.split(".", 1)[1]) if value is not None: return value return None 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"}: return "" signature = geometry_signature(raw_object) if key == "pattern.spacing": spacing = _rounded_number(_first_present(signature.get("spacing"), signature.get("pitch"))) if spacing is None or spacing <= 0: return "SCDM 已识别阵列对象,但没有返回可用于编辑的当前阵列间距。" if ( str(signature.get("patternKind") or "").strip().lower() == "body" or str(signature.get("instanceKind") or "").strip().lower() in {"body", "part", "component"} or _int_list(signature.get("bodyIndices")) ): axis = _rounded_vector(signature.get("axis")) if len(axis) != 3: return "SCDM 已识别实体/组件阵列间距,但没有返回线性阵列方向,暂不能稳定改间距。" instances = _pattern_instances(signature.get("patternInstances")) if len(instances) < 3: return "SCDM 已识别实体/组件阵列间距,但没有返回阵列成员实例,暂不能稳定改间距。" if any(not _component_locators(item.get("componentLocators") or item.get("bodyLocators")) for item in instances): return "SCDM 已识别实体/组件阵列间距,但没有返回每个成员的组件实例定位,暂不能稳定改间距。" return "" axis = _rounded_vector(signature.get("axis")) if len(axis) != 3: return "SCDM 已识别阵列间距,但没有返回线性阵列方向,暂不能稳定改间距。" instances = _pattern_instances(signature.get("patternInstances")) if len(instances) < 3: return "SCDM 已识别阵列间距,但没有返回至少三个可定位实例,暂不能稳定改间距。" if any(not _pattern_instance_has_locator(item) for item in instances): return "SCDM 已识别阵列间距,但没有返回每个阵列实例的可定位 Face,暂不能稳定改间距。" return "" if key == "slot.depth": depth = _rounded_number(signature.get("depth")) if depth is None or depth <= 0: return "SCDM 已识别槽对象,但没有返回可用于编辑的当前槽深。" if not _slot_depth_axis(signature): return "SCDM 已识别槽深,但没有返回槽深方向,暂不能稳定改槽深。" if not _has_depth_face_locator(signature): return "SCDM 已识别槽深,但没有返回可推动的槽底面定位信息,暂不能稳定改槽深。" return "" if key == "round.radius": if not _has_scdm_face_locator(signature): return "SCDM 已识别圆角半径,但没有返回可定位的圆角面,暂不能稳定改半径。" if signature.get("isConstantRound") is not True: return "SCDM 已识别圆角半径,但没有返回等半径圆角证据,暂不能稳定改半径。" return "" if key == "chamfer.distance": if not _has_scdm_face_locator(signature): return "SCDM 已识别倒角距离,但没有返回可定位的倒角面,暂不能稳定改距离。" distance = _rounded_number(signature.get("distance")) if distance is None or distance <= 0: return "SCDM 已识别倒角对象,但没有返回可用于编辑的当前倒角距离。" if signature.get("isEqualDistanceChamfer") is not True: return "SCDM 已识别倒角距离,但没有返回等距倒角证据,暂不能稳定改距离。" return "" if key == "boss.height": if ( _face_locators(signature.get("heightFaceLocators")) or _int_list(signature.get("heightFaceOrdinals")) or _int_list(signature.get("globalHeightFaceOrdinals")) ): return "" return "SCDM 已识别凸台高度,但没有返回可推动的顶面定位信息,暂不能稳定改高度。" if ( _face_locators(signature.get("diameterFaceLocators")) or _int_list(signature.get("diameterFaceOrdinals")) or _int_list(signature.get("globalDiameterFaceOrdinals")) ): return "" return "SCDM 已识别凸台直径,但没有返回可偏移的侧壁定位信息,暂不能稳定改直径。" def _has_scdm_face_locator(signature: Mapping[str, object]) -> bool: if _face_locators(signature.get("scdmFaceLocators")): return True if _int_list(signature.get("faceOrdinals")) or _int_list(signature.get("globalFaceOrdinals")): return True if _int_or_none(signature.get("faceOrdinal")) is not None: return True return _int_or_none(signature.get("globalFaceOrdinal")) is not None def _has_depth_face_locator(signature: Mapping[str, object]) -> bool: if _face_locators(signature.get("depthFaceLocators")): return True if _int_list(signature.get("depthFaceOrdinals")) or _int_list(signature.get("globalDepthFaceOrdinals")): return True return False def _slot_depth_axis(signature: Mapping[str, object]) -> list[float]: return _rounded_vector(signature.get("depthAxis")) def _pattern_instance_has_locator(instance: Mapping[str, object]) -> bool: if _component_locators(instance.get("componentLocators") or instance.get("bodyLocators")): return True if _body_locators(instance.get("bodyLocators")): return True if _int_or_none(instance.get("bodyIndex")) is not None and str(instance.get("instanceKind") or "").lower() in {"body", "part", "component"}: return True if _face_locators(instance.get("scdmFaceLocators")): return True if _int_list(instance.get("faceOrdinals")) or _int_list(instance.get("globalFaceOrdinals")): return True return False def _first_non_empty(*values: str) -> str: for value in values: text = str(value or "").strip() if text: return text return "" def _available_command_names(value: object) -> set[str] | None: if not isinstance(value, list): return None result: set[str] = set() for item in value: if not isinstance(item, Mapping): continue if item.get("available") is not True: continue name = str(item.get("name") or "").strip() if name: result.add(name) return result def _missing_backend_command_reason(key: str, available_command_names: set[str] | None) -> str: if available_command_names is None: return "" definition = capability_definition(key) groups = definition.required_backend_command_groups if definition is not None else () if not groups: return "" if any(all(command in available_command_names for command in group) for group in groups): return "" readable = " / ".join(" + ".join(group) for group in groups) return f"SCDM 当前脚本环境缺少 {readable} 命令,暂不能执行该参数。" def _object_id(raw_object: Mapping[str, object]) -> str: existing = str(raw_object.get("objectId") or "").strip() if existing: return existing object_type = str(raw_object.get("objectType") or "object").strip() or "object" backend_id = str(raw_object.get("backendId") or "").strip() if backend_id: return f"{object_type}:{backend_id}" signature = geometry_signature(raw_object) face_ids = signature.get("faceIds") if isinstance(face_ids, list) and face_ids: return f"{object_type}:face:{'-'.join(str(item) for item in face_ids)}" return object_type def _locator_from_raw_object(raw_object: Mapping[str, object]) -> dict[str, object]: geometry = _mapping(raw_object.get("geometry")) topology = _mapping(raw_object.get("topologyHint")) locator = { "backendId": str(raw_object.get("backendId") or ""), "bodyIndex": _int_or_none(_first_present(topology.get("bodyIndex"), geometry.get("bodyIndex"))), "faceOrdinal": _int_or_none(_first_present(topology.get("faceOrdinal"), geometry.get("faceOrdinal"))), "globalFaceOrdinal": _int_or_none(_first_present(topology.get("globalFaceOrdinal"), geometry.get("globalFaceOrdinal"))), } component_locators = _component_locators(topology.get("componentLocators") or geometry.get("componentLocators")) if component_locators: locator["componentLocators"] = component_locators return locator def _face_locators(value: object) -> list[dict[str, object]]: if not isinstance(value, (list, tuple)): return [] result: list[dict[str, object]] = [] for item in value: if not isinstance(item, Mapping): continue locator = { "backendId": str(item.get("backendId") or ""), "bodyIndex": _int_or_none(item.get("bodyIndex")), "faceOrdinal": _int_or_none(item.get("faceOrdinal")), "globalFaceOrdinal": _int_or_none(item.get("globalFaceOrdinal")), } if any(locator.get(key) is not None for key in ("bodyIndex", "faceOrdinal", "globalFaceOrdinal")): result.append(locator) return result def _body_locators(value: object) -> list[dict[str, object]]: if not isinstance(value, (list, tuple)): return [] result: list[dict[str, object]] = [] seen: set[tuple[object, ...]] = set() for item in value: if not isinstance(item, Mapping): continue locator = { "backendId": str(item.get("backendId") or ""), "bodyIndex": _int_or_none(item.get("bodyIndex")), "componentIndex": _int_or_none(item.get("componentIndex")), "componentPath": _ordered_int_list(item.get("componentPath")), "componentBodyIndex": _int_or_none(item.get("componentBodyIndex")), "componentName": str(item.get("componentName") or ""), } if locator.get("bodyIndex") is not None or locator.get("componentIndex") is not None or locator.get("componentPath"): key = ( locator.get("bodyIndex"), locator.get("componentIndex"), tuple(locator.get("componentPath") or []), locator.get("componentBodyIndex"), ) if key in seen: continue seen.add(key) result.append(locator) return result def _component_locators(value: object) -> list[dict[str, object]]: if not isinstance(value, (list, tuple)): return [] result: list[dict[str, object]] = [] seen: set[tuple[object, ...]] = set() for item in value: if not isinstance(item, Mapping): continue path = _ordered_int_list(item.get("componentPath")) locator = { "backendId": str(item.get("backendId") or ""), "componentIndex": _int_or_none(item.get("componentIndex")), "componentPath": path, "componentBodyIndex": _int_or_none(item.get("componentBodyIndex")), "bodyIndex": _int_or_none(item.get("bodyIndex")), "componentName": str(item.get("componentName") or ""), "contentMoniker": str(item.get("contentMoniker") or ""), "templateMoniker": str(item.get("templateMoniker") or ""), "placementTranslation": _rounded_vector(item.get("placementTranslation")), } if locator.get("componentIndex") is None and not path: continue key = ( locator.get("componentIndex"), tuple(path), locator.get("componentBodyIndex"), locator.get("bodyIndex"), locator.get("contentMoniker"), locator.get("templateMoniker"), ) if key in seen: continue seen.add(key) result.append(locator) return result def _first_int(values: Iterable[object]) -> int | None: for value in values: number = _int_or_none(value) if number is not None: return number return None def _merge_ints(values: Iterable[object]) -> list[int]: result: list[int] = [] for value in values: result.extend(_int_list(value)) return sorted(set(result)) def _mapping(value: object) -> Mapping[str, object]: return value if isinstance(value, Mapping) else {} def _path_value(mapping: Mapping[str, object], path: str) -> object: current: object = mapping for part in path.split("."): if not isinstance(current, Mapping): return None current = current.get(part) if current is None: return None return current def _int_list(value: object) -> list[int]: if isinstance(value, (str, bytes)) or value is None: return [] try: values = list(value) # type: ignore[arg-type] except TypeError: return [] result: list[int] = [] for item in values: try: result.append(int(item)) except (TypeError, ValueError): continue return sorted(set(result)) def _ordered_int_list(value: object) -> list[int]: if isinstance(value, (str, bytes)) or value is None: return [] try: values = list(value) # type: ignore[arg-type] except TypeError: return [] result: list[int] = [] for item in values: try: result.append(int(item)) except (TypeError, ValueError): continue return result def _int_or_none(value: object) -> int | None: try: return int(value) except (TypeError, ValueError): return None def _bool_or_none(value: object) -> bool | None: if isinstance(value, bool): return value if isinstance(value, str): text = value.strip().lower() if text in {"true", "1", "yes", "y"}: return True if text in {"false", "0", "no", "n"}: return False if isinstance(value, (int, float)): return bool(value) return None def _first_present(*values: object) -> object: for value in values: if value is not None and value != "": return value return None def _surface_key(value: object) -> str: text = str(value or "").strip().lower() if "plane" in text: return "plane" if "cylinder" in text: return "cylinder" return text def _canonical_axis(axis: list[float]) -> list[float]: if len(axis) != 3: return [] length = math.sqrt(sum(item * item for item in axis)) if length <= 1e-12: return [] normalized = [item / length for item in axis] for value in normalized: if abs(value) > 1e-12: if value < 0: normalized = [-item for item in normalized] break return [round(item, 6) for item in normalized] def _axis_match_score(left: list[float], right: list[float]) -> float: if len(left) != 3 or len(right) != 3: return 0.0 left_len = math.sqrt(sum(item * item for item in left)) right_len = math.sqrt(sum(item * item for item in right)) if left_len <= 1e-12 or right_len <= 1e-12: return 0.0 dot = abs(sum(left[index] * right[index] for index in range(3)) / (left_len * right_len)) if dot >= 0.999: return 3.0 if dot >= 0.99: return 2.0 return 0.0 def _scaled_number_match_score(local_value: float | None, scdm_value: float | None, scale: float) -> float: if local_value is None or scdm_value is None: return 0.0 error = abs(float(local_value) * scale - float(scdm_value)) reference = max(abs(float(scdm_value)), abs(float(local_value) * scale), 1.0e-9) relative = error / reference if relative <= 1.0e-5: return 4.0 if relative <= 1.0e-3: return 3.0 if relative <= 1.0e-2: return 1.0 return 0.0 def _scaled_vector_match_score(local_value: list[float], scdm_value: list[float], scale: float) -> float: if len(local_value) != 3 or len(scdm_value) != 3: return 0.0 distance = math.sqrt(sum((local_value[index] * scale - scdm_value[index]) ** 2 for index in range(3))) reference = max(max(abs(item) for item in scdm_value), 1.0e-9) relative = distance / reference if relative <= 1.0e-5: return 4.0 if relative <= 1.0e-3: return 3.0 if relative <= 1.0e-2: return 1.0 return 0.0 def _axis_point_distance_score( local_point: list[float], local_axis: list[float], scdm_point: list[float], scale: float, ) -> float: if len(local_point) != 3 or len(local_axis) != 3 or len(scdm_point) != 3: return 0.0 point = [local_point[index] * scale for index in range(3)] axis_len = math.sqrt(sum(item * item for item in local_axis)) if axis_len <= 1e-12: return 0.0 axis = [item / axis_len for item in local_axis] delta = [scdm_point[index] - point[index] for index in range(3)] projection = sum(delta[index] * axis[index] for index in range(3)) nearest = [point[index] + projection * axis[index] for index in range(3)] distance = math.sqrt(sum((nearest[index] - scdm_point[index]) ** 2 for index in range(3))) reference = max(max(abs(item) for item in scdm_point), 1.0e-9) relative = distance / reference if relative <= 1.0e-5: return 3.0 if relative <= 1.0e-3: return 2.0 if relative <= 1.0e-2: return 0.75 return 0.0 def _rounded_vector(value: object) -> list[float]: if isinstance(value, (str, bytes)) or value is None: return [] try: values = list(value) # type: ignore[arg-type] except TypeError: return [] result: list[float] = [] for item in values[:3]: number = _number(item) if number is None: return [] result.append(round(number, 6)) return result if len(result) == 3 else [] def _rounded_vector_list(value: object) -> list[list[float]]: if isinstance(value, (str, bytes)) or value is None: return [] try: values = list(value) # type: ignore[arg-type] except TypeError: return [] result: list[list[float]] = [] for item in values: rounded = _rounded_vector(item) if len(rounded) == 3: result.append(rounded) return result def _pattern_instances(value: object) -> list[dict[str, object]]: if isinstance(value, (str, bytes)) or value is None: return [] try: values = list(value) # type: ignore[arg-type] except TypeError: return [] result: list[dict[str, object]] = [] for item in values: if not isinstance(item, Mapping): continue center = _rounded_vector(item.get("center") or item.get("instanceCenter")) if len(center) != 3: continue result.append( { "sourceObjectId": str(item.get("sourceObjectId") or item.get("objectId") or ""), "instanceKind": str(item.get("instanceKind") or ""), "center": center, "bodyIndex": _int_or_none(item.get("bodyIndex")), "bodyLocators": _body_locators(item.get("bodyLocators")), "componentLocators": _component_locators(item.get("componentLocators") or item.get("bodyLocators")), "faceIds": _int_list(item.get("faceIds")), "faceOrdinals": _int_list(item.get("faceOrdinals") or [item.get("faceOrdinal")]), "globalFaceOrdinals": _int_list( item.get("globalFaceOrdinals") or [item.get("globalFaceOrdinal")] ), "scdmFaceLocators": _face_locators(item.get("scdmFaceLocators") or item.get("faceLocators")), } ) return result def _string_list(value: object) -> list[str]: if isinstance(value, (str, bytes)) or value is None: return [] try: values = list(value) # type: ignore[arg-type] except TypeError: return [] result: list[str] = [] for item in values: text = str(item or "").strip() if text: result.append(text) return result def _rounded_number(value: object) -> float | None: number = _number(value) return None if number is None else round(number, 6) def _number(value: object) -> float | None: try: return float(value) except (TypeError, ValueError): return None __all__ = [ "attach_local_face_ids_to_scdm_cache", "geometry_signature", "map_scdm_raw_features", "map_scdm_raw_features_file", "SCDM_FEATURE_CACHE_REVISION", ]