import json import sys def get_script_arguments(argv): if "--pass" in argv: pass_index = argv.index("--pass") script_args = argv[pass_index + 1 :] else: script_args = argv[2:] if len(script_args) < 3: raise ValueError("Expected input path, deleted paths manifest, and output path.") return script_args[0], script_args[1], script_args[2] def should_include_in_tree(document_object): type_id = getattr(document_object, "TypeId", "") return type_id not in {"App::Origin", "App::Line", "App::Plane", "Part::Feature"} def get_tree_children(objects): filtered_objects = [document_object for document_object in objects if should_include_in_tree(document_object)] def sort_key(document_object): child_count = len(get_tree_children(list(getattr(document_object, "OutList", [])))) is_leaf_part = child_count == 0 return (1 if is_leaf_part else 0) filtered_objects.sort(key=sort_key) return filtered_objects def enumerate_objects(objects, parent_path=""): filtered_objects = get_tree_children(objects) for index, document_object in enumerate(filtered_objects): object_path = str(index) if parent_path == "" else parent_path + "/" + str(index) yield object_path, document_object child_objects = list(getattr(document_object, "OutList", [])) yield from enumerate_objects(child_objects, object_path) def collect_all_subtree_objects(objects): collected_objects = [] for document_object in objects: collected_objects.append(document_object) child_objects = list(getattr(document_object, "OutList", [])) collected_objects.extend(collect_all_subtree_objects(child_objects)) return collected_objects def collect_subtree_objects(objects, target_path, parent_path=""): collected_objects = [] filtered_objects = get_tree_children(objects) for index, document_object in enumerate(filtered_objects): object_path = str(index) if parent_path == "" else parent_path + "/" + str(index) child_objects = list(getattr(document_object, "OutList", [])) if target_path is None: collected_objects.append(document_object) collected_objects.extend(collect_all_subtree_objects(child_objects)) elif object_path == target_path: collected_objects.append(document_object) collected_objects.extend(collect_all_subtree_objects(child_objects)) elif target_path.startswith(object_path + "/"): collected_objects.extend(collect_subtree_objects(child_objects, target_path, object_path)) return collected_objects def build_removal_names(objects_to_delete): removal_candidates = [] seen_names = set() for document_object in objects_to_delete: object_name = getattr(document_object, "Name", None) if object_name is None or object_name in seen_names: continue seen_names.add(object_name) subtree_depth = len(list(getattr(document_object, "OutListRecursive", []))) removal_candidates.append((object_name, subtree_depth)) removal_candidates.sort(key=lambda candidate: candidate[1], reverse=True) return [object_name for object_name, _depth in removal_candidates] def main(): input_path, deleted_paths_path, output_path = get_script_arguments(sys.argv) with open(deleted_paths_path, "r", encoding="utf-8") as deleted_file: deleted_paths = set(json.load(deleted_file)) import FreeCAD import Import document = FreeCAD.newDocument("TrimmedStep") Import.insert(input_path, document.Name) objects_to_delete = [] root_objects = list(document.RootObjects) for deleted_path in deleted_paths: objects_to_delete.extend(collect_subtree_objects(root_objects, deleted_path)) for object_name in build_removal_names(objects_to_delete): if document.getObject(object_name) is None: continue document.removeObject(object_name) Import.export(list(document.RootObjects), output_path) FreeCAD.closeDocument(document.Name) if __name__ in ("__main__", "freecad_trim_step"): main()