feat: complete production naming and reference lifecycle gates
This commit is contained in:
231
scripts/freecad-xlink-relink-oracle.py
Normal file
231
scripts/freecad-xlink-relink-oracle.py
Normal file
@@ -0,0 +1,231 @@
|
||||
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=(",", ":")))
|
||||
Reference in New Issue
Block a user