feat: 接入 SCDM 优先编辑闭环并支持阵列局部间距
接入 SCDM probe/edit/cache/校验链路,增强孔组、阵列、关系式和参数表交互。 支持阵列相邻段间距、移动意图切换、结果回滚校验,并补充对应回归脚本。
This commit is contained in:
+296
-4
@@ -241,9 +241,18 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" geometry['surfaceType'] = 'plane'\n"
|
||||
" elif 'cylinder' in lowered:\n"
|
||||
" geometry['surfaceType'] = 'cylinder'\n"
|
||||
" slot_info = _slot_info_from_face(face, geometry)\n"
|
||||
" if slot_info:\n"
|
||||
" geometry['slotInfo'] = slot_info\n"
|
||||
" for key in ('width', 'depth', 'center', 'depthAxis'):\n"
|
||||
" if slot_info.get(key) is not None:\n"
|
||||
" geometry[key] = slot_info.get(key)\n"
|
||||
" round_info = _round_info_from_face(face, geometry)\n"
|
||||
" if round_info:\n"
|
||||
" geometry['roundInfo'] = round_info\n"
|
||||
" chamfer_info = _chamfer_info_from_face(face, geometry)\n"
|
||||
" if chamfer_info:\n"
|
||||
" geometry['chamferInfo'] = chamfer_info\n"
|
||||
" return geometry\n"
|
||||
"\n"
|
||||
"def _round_info_from_face(face, geometry):\n"
|
||||
@@ -274,6 +283,101 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" pass\n"
|
||||
" return payload\n"
|
||||
"\n"
|
||||
"def _same_object(left, right):\n"
|
||||
" try:\n"
|
||||
" if left is right:\n"
|
||||
" return True\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" try:\n"
|
||||
" return left == right\n"
|
||||
" except Exception:\n"
|
||||
" return False\n"
|
||||
"\n"
|
||||
"def _slot_info_from_face(face, geometry):\n"
|
||||
" slot_info_type = globals().get('SlotInfo')\n"
|
||||
" if slot_info_type is None:\n"
|
||||
" return {}\n"
|
||||
" try:\n"
|
||||
" info = slot_info_type.Create(face)\n"
|
||||
" except Exception:\n"
|
||||
" return {}\n"
|
||||
" payload = {'available': True, 'type': _safe_name(info)}\n"
|
||||
" for key, attrs in (\n"
|
||||
" ('width', ('Width', 'SlotWidth', 'Diameter')),\n"
|
||||
" ('depth', ('Depth', 'SlotDepth', 'Height')),\n"
|
||||
" ):\n"
|
||||
" value = _float_attr(info, attrs)\n"
|
||||
" if value is not None:\n"
|
||||
" payload[key] = value\n"
|
||||
" center = _xyz(_first_path_value(info, ('Center', 'AxisCenter', 'Frame.Origin')))\n"
|
||||
" if center:\n"
|
||||
" payload['center'] = center\n"
|
||||
" depth_axis = _xyz(_first_path_value(info, ('DepthAxis', 'DepthDirection', 'Direction', 'Frame.DirZ')))\n"
|
||||
" if depth_axis:\n"
|
||||
" payload['depthAxis'] = depth_axis\n"
|
||||
" for attr in ('IsBlind', 'IsThrough', 'IsSlot'):\n"
|
||||
" try:\n"
|
||||
" payload[attr[0].lower() + attr[1:]] = bool(getattr(info, attr))\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" for attr in ('BottomFace', 'DepthFace', 'FloorFace'):\n"
|
||||
" try:\n"
|
||||
" if _same_object(getattr(info, attr), face):\n"
|
||||
" payload['depthFaceIsCurrent'] = True\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" for attr in ('BottomFaces', 'DepthFaces', 'FloorFaces'):\n"
|
||||
" try:\n"
|
||||
" for item in _items(getattr(info, attr)):\n"
|
||||
" if _same_object(item, face):\n"
|
||||
" payload['depthFaceIsCurrent'] = True\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" try:\n"
|
||||
" payload['attributes'] = [name for name in dir(info) if not name.startswith('_')][:40]\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return payload\n"
|
||||
"\n"
|
||||
"def _chamfer_info_from_face(face, geometry):\n"
|
||||
" if str(geometry.get('surfaceType', '')).lower() != 'plane':\n"
|
||||
" return {}\n"
|
||||
" chamfer_info_type = globals().get('ChamferInfo')\n"
|
||||
" if chamfer_info_type is None:\n"
|
||||
" return {}\n"
|
||||
" try:\n"
|
||||
" info = chamfer_info_type.Create(face)\n"
|
||||
" except Exception:\n"
|
||||
" return {}\n"
|
||||
" payload = {'available': True, 'type': _safe_name(info)}\n"
|
||||
" for attr in ('Distance', 'ChamferDistance', 'Offset', 'Width'):\n"
|
||||
" value = _float_attr(info, (attr, attr[0].lower() + attr[1:]))\n"
|
||||
" if value is not None:\n"
|
||||
" payload['distance'] = value\n"
|
||||
" break\n"
|
||||
" distance1 = _float_attr(info, ('Distance1', 'distance1', 'FirstDistance'))\n"
|
||||
" distance2 = _float_attr(info, ('Distance2', 'distance2', 'SecondDistance'))\n"
|
||||
" if distance1 is not None:\n"
|
||||
" payload['distance1'] = distance1\n"
|
||||
" if distance2 is not None:\n"
|
||||
" payload['distance2'] = distance2\n"
|
||||
" if distance1 is not None and distance2 is not None and abs(distance1 - distance2) <= max(abs(distance1), abs(distance2), 1.0) * 1e-6:\n"
|
||||
" payload.setdefault('distance', distance1)\n"
|
||||
" payload['isEqualDistance'] = True\n"
|
||||
" for attr in ('IsEqualDistance', 'IsSymmetric', 'IsChamfer'):\n"
|
||||
" try:\n"
|
||||
" payload[attr[0].lower() + attr[1:]] = bool(getattr(info, attr))\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" if payload.get('isSymmetric') is True:\n"
|
||||
" payload['isEqualDistance'] = True\n"
|
||||
" try:\n"
|
||||
" payload['attributes'] = [name for name in dir(info) if not name.startswith('_')][:40]\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return payload\n"
|
||||
"\n"
|
||||
"def _path_value(value, expr):\n"
|
||||
" current = value\n"
|
||||
" for part in expr.split('.'):\n"
|
||||
@@ -462,10 +566,21 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" result.append({'operation': 'move_hole_axis', 'enabled': True, 'parameterFields': {'center': geometry.get('center')}})\n"
|
||||
" if object_type == 'hole' and surface_type == 'cylinder':\n"
|
||||
" result.append({'operation': 'fill_feature', 'enabled': True, 'parameterFields': {}})\n"
|
||||
" if object_type in ('slot', 'obround_slot', 'rectangular_slot'):\n"
|
||||
" if geometry.get('width') is not None:\n"
|
||||
" result.append({'operation': 'change_slot_width', 'enabled': True, 'parameterFields': {'width': geometry.get('width')}})\n"
|
||||
" if geometry.get('depth') is not None:\n"
|
||||
" result.append({'operation': 'change_slot_depth', 'enabled': True, 'parameterFields': {'depth': geometry.get('depth')}})\n"
|
||||
" if geometry.get('center') is not None:\n"
|
||||
" result.append({'operation': 'move_slot', 'enabled': True, 'parameterFields': {'center': geometry.get('center')}})\n"
|
||||
" round_info = geometry.get('roundInfo')\n"
|
||||
" if isinstance(round_info, dict) and round_info.get('radius') is not None:\n"
|
||||
" result.append({'operation': 'change_round_radius', 'enabled': True, 'parameterFields': {'radius': round_info.get('radius')}})\n"
|
||||
" result.append({'operation': 'delete_round_or_chamfer', 'enabled': True, 'parameterFields': {}})\n"
|
||||
" chamfer_info = geometry.get('chamferInfo')\n"
|
||||
" if isinstance(chamfer_info, dict) and chamfer_info.get('distance') is not None:\n"
|
||||
" result.append({'operation': 'change_chamfer_distance', 'enabled': True, 'parameterFields': {'distance': chamfer_info.get('distance')}})\n"
|
||||
" result.append({'operation': 'delete_round_or_chamfer', 'enabled': True, 'parameterFields': {}})\n"
|
||||
" return result\n"
|
||||
"\n"
|
||||
"def _open_step(path):\n"
|
||||
@@ -503,6 +618,167 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" except Exception:\n"
|
||||
" return value\n"
|
||||
"\n"
|
||||
"def _safe_str(value):\n"
|
||||
" if value is None:\n"
|
||||
" return ''\n"
|
||||
" try:\n"
|
||||
" return str(value)\n"
|
||||
" except Exception:\n"
|
||||
" return _safe_name(value)\n"
|
||||
"\n"
|
||||
"def _matrix_payload(matrix):\n"
|
||||
" if matrix is None:\n"
|
||||
" return {}\n"
|
||||
" payload = {'type': _safe_name(matrix), 'text': _safe_str(matrix)}\n"
|
||||
" translation = _xyz(_path_value(matrix, 'Translation'))\n"
|
||||
" if translation:\n"
|
||||
" payload['translation'] = translation\n"
|
||||
" for attr in ('OffsetX', 'OffsetY', 'OffsetZ'):\n"
|
||||
" value = _float_attr(matrix, (attr, attr[0].lower() + attr[1:]))\n"
|
||||
" if value is not None:\n"
|
||||
" payload[attr] = value\n"
|
||||
" return payload\n"
|
||||
"\n"
|
||||
"def _moniker_text(value):\n"
|
||||
" try:\n"
|
||||
" return _safe_str(getattr(value, 'Moniker'))\n"
|
||||
" except Exception:\n"
|
||||
" return ''\n"
|
||||
"\n"
|
||||
"def _component_name(component):\n"
|
||||
" for attr in ('Name', 'DisplayName'):\n"
|
||||
" try:\n"
|
||||
" text = _safe_str(getattr(component, attr)).strip()\n"
|
||||
" if text:\n"
|
||||
" return text\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return ''\n"
|
||||
"\n"
|
||||
"def _immediate_components(part):\n"
|
||||
" if part is None:\n"
|
||||
" return []\n"
|
||||
" try:\n"
|
||||
" items = _items(getattr(part, 'Components'))\n"
|
||||
" if items:\n"
|
||||
" return items\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return []\n"
|
||||
"\n"
|
||||
"def _component_content(component):\n"
|
||||
" for attr in ('Content', 'ContentMaster', 'Template', 'Part'):\n"
|
||||
" try:\n"
|
||||
" value = getattr(component, attr)\n"
|
||||
" if value is not None:\n"
|
||||
" return value\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return None\n"
|
||||
"\n"
|
||||
"def _component_locator(component, component_index, component_path):\n"
|
||||
" locator = {\n"
|
||||
" 'backendId': 'component:' + '.'.join(str(item) for item in component_path),\n"
|
||||
" 'componentIndex': component_index,\n"
|
||||
" 'componentPath': list(component_path),\n"
|
||||
" 'componentName': _component_name(component),\n"
|
||||
" }\n"
|
||||
" try:\n"
|
||||
" locator['componentMoniker'] = _moniker_text(component)\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" try:\n"
|
||||
" content = getattr(component, 'Content')\n"
|
||||
" locator['contentMoniker'] = _moniker_text(content)\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" try:\n"
|
||||
" template = getattr(component, 'Template')\n"
|
||||
" locator['templateMoniker'] = _moniker_text(template)\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" try:\n"
|
||||
" placement = _matrix_payload(getattr(component, 'Placement'))\n"
|
||||
" if placement:\n"
|
||||
" locator['placement'] = placement\n"
|
||||
" if placement.get('translation'):\n"
|
||||
" locator['placementTranslation'] = placement.get('translation')\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return locator\n"
|
||||
"\n"
|
||||
"def _component_entries(root):\n"
|
||||
" result = []\n"
|
||||
" queue = [(root, [])]\n"
|
||||
" while queue:\n"
|
||||
" part, path = queue.pop(0)\n"
|
||||
" if part is None or len(path) > 8:\n"
|
||||
" continue\n"
|
||||
" for child_index, component in enumerate(_immediate_components(part)):\n"
|
||||
" component_path = list(path) + [child_index]\n"
|
||||
" content = _component_content(component)\n"
|
||||
" entry = {\n"
|
||||
" 'component': component,\n"
|
||||
" 'content': content,\n"
|
||||
" 'locator': _component_locator(component, len(result), component_path),\n"
|
||||
" }\n"
|
||||
" result.append(entry)\n"
|
||||
" if content is not None:\n"
|
||||
" queue.append((content, component_path))\n"
|
||||
" return result\n"
|
||||
"\n"
|
||||
"def _component_body_locator_map(component_entries):\n"
|
||||
" result = {}\n"
|
||||
" for entry in component_entries:\n"
|
||||
" content = entry.get('content')\n"
|
||||
" if content is None:\n"
|
||||
" continue\n"
|
||||
" for component_body_index, body in enumerate(_items(_maybe_call(content, 'Bodies'))):\n"
|
||||
" locator = dict(entry.get('locator') or {})\n"
|
||||
" locator['componentBodyIndex'] = component_body_index\n"
|
||||
" key = str(id(body))\n"
|
||||
" result.setdefault(key, []).append(locator)\n"
|
||||
" try:\n"
|
||||
" master = getattr(body, 'Master')\n"
|
||||
" result.setdefault(str(id(master)), []).append(locator)\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" return result\n"
|
||||
"\n"
|
||||
"def _body_locators_for_body(component_body_locators, body, body_index):\n"
|
||||
" result = [{'bodyIndex': body_index}]\n"
|
||||
" seen = set(['body:' + str(body_index)])\n"
|
||||
" for locator in component_body_locators.get(str(id(body)), []) or []:\n"
|
||||
" item = dict(locator)\n"
|
||||
" item['bodyIndex'] = body_index\n"
|
||||
" key = str(item.get('componentIndex')) + ':' + '.'.join(str(value) for value in item.get('componentPath', []) or []) + ':' + str(item.get('componentBodyIndex'))\n"
|
||||
" if key in seen:\n"
|
||||
" continue\n"
|
||||
" seen.add(key)\n"
|
||||
" result.append(item)\n"
|
||||
" return result\n"
|
||||
"\n"
|
||||
"def _component_locators_for_body(component_body_locators, body):\n"
|
||||
" result = []\n"
|
||||
" seen = set()\n"
|
||||
" for locator in component_body_locators.get(str(id(body)), []) or []:\n"
|
||||
" key = str(locator.get('componentIndex')) + ':' + '.'.join(str(value) for value in locator.get('componentPath', []) or []) + ':' + str(locator.get('componentBodyIndex'))\n"
|
||||
" if key in seen:\n"
|
||||
" continue\n"
|
||||
" seen.add(key)\n"
|
||||
" result.append(dict(locator))\n"
|
||||
" return result\n"
|
||||
"\n"
|
||||
"def _component_inventory(component_entries):\n"
|
||||
" result = []\n"
|
||||
" for entry in component_entries:\n"
|
||||
" locator = dict(entry.get('locator') or {})\n"
|
||||
" content = entry.get('content')\n"
|
||||
" locator['contentBodyCount'] = len(_items(_maybe_call(content, 'Bodies'))) if content is not None else 0\n"
|
||||
" locator['childComponentCount'] = len(_immediate_components(content)) if content is not None else 0\n"
|
||||
" result.append(locator)\n"
|
||||
" return result\n"
|
||||
"\n"
|
||||
"def _body_faces(body):\n"
|
||||
" for name in ('Faces', 'GetFaces'):\n"
|
||||
" items = _items(_maybe_call(body, name))\n"
|
||||
@@ -596,7 +872,7 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" return set(str(id(face)) for face in faces)\n"
|
||||
"\n"
|
||||
"def _available_commands():\n"
|
||||
" names = ('StandardHoles', 'OffsetFaces', 'Move', 'Fill', 'Delete', 'Chamfer', 'ConstantRound', 'RoundInfo')\n"
|
||||
" names = ('StandardHoles', 'OffsetFaces', 'Move', 'Fill', 'Delete', 'Chamfer', 'ConstantRound', 'RoundInfo', 'ChamferInfo', 'SlotInfo')\n"
|
||||
" result = []\n"
|
||||
" for name in names:\n"
|
||||
" result.append({'name': name, 'available': globals().get(name) is not None})\n"
|
||||
@@ -612,6 +888,8 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" _open_step(model.get('path'))\n"
|
||||
" root = _root_part()\n"
|
||||
" bodies = _all_bodies(root)\n"
|
||||
" component_entries = _component_entries(root)\n"
|
||||
" component_body_locators = _component_body_locator_map(component_entries)\n"
|
||||
" hole_face_markers = _hole_face_markers(bodies)\n"
|
||||
" objects = []\n"
|
||||
" face_adjacency = {}\n"
|
||||
@@ -620,24 +898,37 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" edge_counter = 0\n"
|
||||
" for body_index, body in enumerate(bodies):\n"
|
||||
" body_faces = _body_faces(body)\n"
|
||||
" body_locators = _body_locators_for_body(component_body_locators, body, body_index)\n"
|
||||
" component_locators = _component_locators_for_body(component_body_locators, body)\n"
|
||||
" face_ordinals_by_marker = dict((str(id(face)), index) for index, face in enumerate(body_faces))\n"
|
||||
" for face_index, face in enumerate(body_faces):\n"
|
||||
" geometry = _geometry_from_face(face)\n"
|
||||
" object_type = 'hole' if str(id(face)) in hole_face_markers else 'face'\n"
|
||||
" if object_type == 'face' and isinstance(geometry.get('slotInfo'), dict) and (geometry.get('depth') is not None or geometry.get('width') is not None):\n"
|
||||
" object_type = 'slot'\n"
|
||||
" if object_type == 'face' and isinstance(geometry.get('roundInfo'), dict) and geometry.get('roundInfo', {}).get('radius') is not None:\n"
|
||||
" object_type = 'round'\n"
|
||||
" if object_type == 'face' and isinstance(geometry.get('chamferInfo'), dict) and geometry.get('chamferInfo', {}).get('distance') is not None:\n"
|
||||
" object_type = 'chamfer'\n"
|
||||
" topology_hint = {'bodyIndex': body_index, 'faceOrdinal': face_index, 'globalFaceOrdinal': face_counter, 'bodyLocators': body_locators}\n"
|
||||
" if component_locators:\n"
|
||||
" topology_hint['componentLocators'] = component_locators\n"
|
||||
" if object_type == 'slot' and isinstance(geometry.get('slotInfo'), dict) and geometry.get('slotInfo', {}).get('depthFaceIsCurrent') is True:\n"
|
||||
" topology_hint['depthFaceLocators'] = [dict(topology_hint)]\n"
|
||||
" objects.append({\n"
|
||||
" 'backendId': 'body:%d/face:%d' % (body_index, face_index),\n"
|
||||
" 'objectType': object_type,\n"
|
||||
" 'geometry': geometry,\n"
|
||||
" 'topologyHint': {'bodyIndex': body_index, 'faceOrdinal': face_index, 'globalFaceOrdinal': face_counter},\n"
|
||||
" 'topologyHint': topology_hint,\n"
|
||||
" 'backendCommandCandidates': _command_candidates(object_type, geometry),\n"
|
||||
" 'rawLimitations': [],\n"
|
||||
" })\n"
|
||||
" face_counter += 1\n"
|
||||
" for edge_index, edge in enumerate(_body_edges(body)):\n"
|
||||
" geometry = _geometry_from_edge(edge)\n"
|
||||
" edge_topology = {'bodyIndex': body_index, 'edgeOrdinal': edge_index, 'globalEdgeOrdinal': edge_counter}\n"
|
||||
" edge_topology = {'bodyIndex': body_index, 'edgeOrdinal': edge_index, 'globalEdgeOrdinal': edge_counter, 'bodyLocators': body_locators}\n"
|
||||
" if component_locators:\n"
|
||||
" edge_topology['componentLocators'] = component_locators\n"
|
||||
" edge_topology.update(_edge_adjacent_face_ordinals(edge, face_ordinals_by_marker))\n"
|
||||
" _add_edge_geometry_summary(edge_geometry_summary, geometry)\n"
|
||||
" _record_face_adjacency(face_adjacency, body_index, edge_topology, geometry)\n"
|
||||
@@ -661,8 +952,9 @@ def generate_scdm_probe_script(job_path: str | Path) -> str:
|
||||
" 'faceAdjacency': _face_adjacency_rows(face_adjacency),\n"
|
||||
" 'edgeGeometrySummary': _final_edge_geometry_summary(edge_geometry_summary),\n"
|
||||
" 'featureInventory': _feature_inventory(objects),\n"
|
||||
" 'componentInstances': _component_inventory(component_entries),\n"
|
||||
" },\n"
|
||||
" 'summary': {'bodyCount': len(bodies), 'objectCount': len(objects), 'faceCount': face_counter, 'edgeCount': edge_counter, 'holeFaceCount': len(hole_face_markers)},\n"
|
||||
" 'summary': {'bodyCount': len(bodies), 'objectCount': len(objects), 'faceCount': face_counter, 'edgeCount': edge_counter, 'holeFaceCount': len(hole_face_markers), 'componentCount': len(component_entries)},\n"
|
||||
" }\n"
|
||||
" _write_json(raw_path, payload)\n"
|
||||
" except Exception as exc:\n"
|
||||
|
||||
Reference in New Issue
Block a user