import json import os import tempfile import FreeCAD as App import Part # noqa: F401 - registers Part and PartDesign shapes FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" def version_text(): return ".".join(str(value) for value in App.Version()[:3]) def document_open(name): return name in App.listDocuments() def object_ref(obj): if obj is None: return None return {"document": obj.Document.Name, "object": obj.Name} def xlink_ref(value): return object_ref(value) def xlink_sub_ref(value): if not value or value[0] is None: return None obj, sub_elements = value subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements) return {**object_ref(obj), "subElements": [str(value) for value in subs if str(value)]} def xlink_list_ref(value): return [object_ref(obj) for obj in value if obj is not None] def xlink_sub_list_ref(value): output = [] for obj, sub_elements in value: if obj is None: continue subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements) output.append({**object_ref(obj), "subElements": [str(value) for value in subs if str(value)]}) return output def property_status(holder, name): return [str(value) for value in holder.getPropertyStatus(name)] def holder_state(holder): return { "xLink": xlink_ref(holder.XLink), "xLinkSub": xlink_sub_ref(holder.XLinkSub), "xLinkList": xlink_list_ref(holder.XLinkList), "xLinkSubList": xlink_sub_list_ref(holder.XLinkSubList), "propertyStatus": { name: property_status(holder, name) for name in ["XLink", "XLinkSub", "XLinkList", "XLinkSubList"] }, "state": [str(value) for value in holder.State], "status": str(holder.getStatusString()), } def binder_state(binder): support = [] for obj, sub_elements in binder.Support: subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements) support.append({**object_ref(obj), "subElements": [str(value) for value in subs if str(value)]}) return { "support": support, "area": float(binder.Shape.Area), "shapeNull": bool(binder.Shape.isNull()), "state": [str(value) for value in binder.State], "status": str(binder.getStatusString()), } def linked_dimensions(holder): return { "xLinkLength": float(holder.XLink.Length) if holder.XLink else None, "xLinkSubWidth": float(holder.XLinkSub[0].Width) if holder.XLinkSub and holder.XLinkSub[0] else None, "xLinkListHeights": [float(obj.Height) for obj in holder.XLinkList if obj is not None], "xLinkSubListAreas": [float(obj.getSubObject(sub).Area) for obj, subs in holder.XLinkSubList for sub in subs], } with tempfile.TemporaryDirectory(prefix="freecad-xlink-relink-") as temp_dir: source_path = os.path.join(temp_dir, "XLinkSource.FCStd") consumer_path = os.path.join(temp_dir, "XLinkConsumer.FCStd") missing_path = os.path.join(temp_dir, "XLinkSource.missing.FCStd") source = App.newDocument("XLinkSource") source_body = source.addObject("PartDesign::Body", "SourceBody") source_box = source_body.newObject("PartDesign::AdditiveBox", "SourceBox") source_box.Length = 4 source_box.Width = 5 source_box.Height = 6 second_body = source.addObject("PartDesign::Body", "SecondBody") second_box = second_body.newObject("PartDesign::AdditiveBox", "SecondBox") second_box.Length = 3 second_box.Width = 7 second_box.Height = 2 source.recompute() source.saveAs(source_path) consumer = App.newDocument("XLinkConsumer") consumer.saveAs(consumer_path) holder = consumer.addObject("App::FeaturePython", "XLinkHolder") holder.addProperty("App::PropertyXLink", "XLink", "External") holder.addProperty("App::PropertyXLinkSub", "XLinkSub", "External") holder.addProperty("App::PropertyXLinkList", "XLinkList", "External") holder.addProperty("App::PropertyXLinkSubList", "XLinkSubList", "External") holder.XLink = source_box holder.XLinkSub = (source_box, ["Face1"]) holder.XLinkList = [source_box, second_box] holder.XLinkSubList = [(source_box, ["Face1"]), (second_box, ["Face1", "Face2"])] binder_body = consumer.addObject("PartDesign::Body", "BinderBody") binder = binder_body.newObject("PartDesign::SubShapeBinder", "ExternalBinder") binder.Support = [(source_box, ("Face1",))] consumer.recompute() consumer.saveAs(consumer_path) source.save() consumer.save() initial = { "holder": holder_state(holder), "dimensions": linked_dimensions(holder), "binder": binder_state(binder), "sourceDocuments": sorted(document.Name for document in App.listDocuments().values()), } App.closeDocument(source.Name) consumer.recompute() source_closed = { "holder": holder_state(holder), "binder": binder_state(binder), "sourceOpen": document_open("XLinkSource"), } reopened_source = App.openDocument(source_path) reopened_source.recompute() consumer.recompute() relink_after_close = { "holder": holder_state(holder), "dimensions": linked_dimensions(holder), "binder": binder_state(binder), "sourceOpen": document_open("XLinkSource") and App.listDocuments()["XLinkSource"] is reopened_source, } App.closeDocument(consumer.Name) App.closeDocument(reopened_source.Name) os.rename(source_path, missing_path) missing_consumer = App.openDocument(consumer_path) missing_consumer.recompute() missing_holder = missing_consumer.getObject("XLinkHolder") missing_binder = missing_consumer.getObject("ExternalBinder") missing_source = { "holder": holder_state(missing_holder), "binder": binder_state(missing_binder), "sourceOpen": document_open("XLinkSource"), } os.rename(missing_path, source_path) restored_source = App.openDocument(source_path) restored_source.recompute() missing_consumer.recompute() restored_holder = missing_consumer.getObject("XLinkHolder") restored_binder = missing_consumer.getObject("ExternalBinder") relink_after_missing = { "holder": holder_state(restored_holder), "dimensions": linked_dimensions(restored_holder), "binder": binder_state(restored_binder), "sourceOpen": document_open("XLinkSource") and App.listDocuments()["XLinkSource"] is restored_source, } restored_box = restored_source.getObject("SourceBox") restored_second = restored_source.getObject("SecondBox") restored_box.Length = 8 restored_box.Width = 9 restored_second.Height = 4 restored_source.recompute() missing_consumer.recompute() edited = { "holder": holder_state(restored_holder), "dimensions": linked_dimensions(restored_holder), "binder": binder_state(restored_binder), "sourceFaceArea": float(restored_box.Shape.Face1.Area), } restored_source.save() missing_consumer.save() App.closeDocument(missing_consumer.Name) App.closeDocument(restored_source.Name) final_source = App.openDocument(source_path) final_consumer = App.openDocument(consumer_path) final_source.recompute() final_consumer.recompute() final_holder = final_consumer.getObject("XLinkHolder") final_binder = final_consumer.getObject("ExternalBinder") final_roundtrip = { "holder": holder_state(final_holder), "dimensions": linked_dimensions(final_holder), "binder": binder_state(final_binder), "sourceFaceArea": float(final_source.getObject("SourceBox").Shape.Face1.Area), } App.closeDocument(final_consumer.Name) App.closeDocument(final_source.Name) report = { "schemaVersion": 1, "baselineId": "freecad-1.1.1-xlink-relink-oracle", "freecadVersion": version_text(), "gitCommit": FREECAD_COMMIT, "status": "pass", "tolerance": 1e-7, "propertyTypes": ["App::PropertyXLink", "App::PropertyXLinkSub", "App::PropertyXLinkList", "App::PropertyXLinkSubList"], "initial": initial, "sourceClosed": source_closed, "relinkAfterClose": relink_after_close, "missingSource": missing_source, "relinkAfterMissing": relink_after_missing, "edited": edited, "finalRoundtrip": final_roundtrip, } print("FREECAD_XLINK_RELINK_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))