import json import os import tempfile import FreeCAD as App def version_text(): return ".".join(str(value) for value in App.Version()[:3]) def names(objects): return [obj.Name for obj in objects] def link_sub_values(value): output = [] for obj, sub_elements in value: if isinstance(sub_elements, str): subs = [sub_elements] if sub_elements else [] else: subs = [str(sub) for sub in sub_elements if str(sub)] output.append({"object": obj.Name, "subElements": subs}) return output def placement_json(placement): axis = placement.Rotation.Axis base = placement.Base return { "position": [float(base.x), float(base.y), float(base.z)], "axis": [float(axis.x), float(axis.y), float(axis.z)], "angleDegrees": float(placement.Rotation.Angle * 180.0 / 3.141592653589793), } document = App.newDocument("PartDesignStructureOracle") source_body = document.addObject("PartDesign::Body", "SourceBody") source_box = source_body.newObject("PartDesign::AdditiveBox", "SourceBox") source_box.Length = 10 source_box.Width = 8 source_box.Height = 6 datum_body = document.addObject("PartDesign::Body", "DatumBody") datum_plane = datum_body.newObject("PartDesign::Plane", "DatumPlane") datum_plane.AttachmentSupport = [(document.XY_Plane, "")] datum_plane.MapMode = "FlatFace" datum_plane.AttachmentOffset = App.Placement(App.Vector(0, 0, 5), App.Rotation(App.Vector(0, 0, 1), 30)) datum_line = datum_body.newObject("PartDesign::Line", "DatumLine") datum_line.AttachmentSupport = [(document.XY_Plane, "")] datum_line.MapMode = "ObjectX" datum_point = datum_body.newObject("PartDesign::Point", "DatumPoint") datum_point.AttachmentSupport = [(document.XY_Plane, "")] datum_point.MapMode = "ObjectOrigin" datum_point.AttachmentOffset = App.Placement(App.Vector(1, 2, 3), App.Rotation()) binder_body = document.addObject("PartDesign::Body", "BinderBody") shape_binder = binder_body.newObject("PartDesign::ShapeBinder", "ShapeBinder") shape_binder.Support = [(source_box, "")] shape_binder.TraceSupport = True sub_binder_body = document.addObject("PartDesign::Body", "SubBinderBody") sub_binder = sub_binder_body.newObject("PartDesign::SubShapeBinder", "SubShapeBinder") sub_binder.Support = [(source_box, ("Edge1", "Edge2", "Edge3", "Edge4"))] document.recompute() initial = { "sourceVolume": float(source_box.Shape.Volume), "shapeBinderVolume": float(shape_binder.Shape.Volume), "subShapeBinderLength": float(sub_binder.Shape.Length), "bodyTips": {body.Name: body.Tip.Name if body.Tip else None for body in [source_body, datum_body, binder_body, sub_binder_body]}, "bodyGroups": {body.Name: names(body.Group) for body in [source_body, datum_body, binder_body, sub_binder_body]}, "datumPlane": {"mapMode": datum_plane.MapMode, "support": link_sub_values(datum_plane.AttachmentSupport), "attachmentOffset": placement_json(datum_plane.AttachmentOffset), "placement": placement_json(datum_plane.Placement), "state": [str(value) for value in datum_plane.State]}, "datumLine": {"mapMode": datum_line.MapMode, "support": link_sub_values(datum_line.AttachmentSupport), "state": [str(value) for value in datum_line.State]}, "datumPoint": {"mapMode": datum_point.MapMode, "support": link_sub_values(datum_point.AttachmentSupport), "attachmentOffset": placement_json(datum_point.AttachmentOffset), "placement": placement_json(datum_point.Placement), "state": [str(value) for value in datum_point.State]}, "shapeBinder": {"support": link_sub_values(shape_binder.Support), "traceSupport": bool(shape_binder.TraceSupport), "state": [str(value) for value in shape_binder.State]}, "subShapeBinder": {"support": link_sub_values(sub_binder.Support), "state": [str(value) for value in sub_binder.State]}, } source_box.Length = 14 datum_plane.AttachmentOffset = App.Placement(App.Vector(0, 0, 7), App.Rotation(App.Vector(0, 0, 1), 45)) document.recompute() edited = { "sourceVolume": float(source_box.Shape.Volume), "shapeBinderVolume": float(shape_binder.Shape.Volume), "subShapeBinderLength": float(sub_binder.Shape.Length), "datumPlaneAttachmentOffset": placement_json(datum_plane.AttachmentOffset), "datumPlanePlacement": placement_json(datum_plane.Placement), "bodyTips": {body.Name: body.Tip.Name if body.Tip else None for body in [source_body, datum_body, binder_body, sub_binder_body]}, } invalid_plane = datum_body.newObject("PartDesign::Plane", "InvalidDatumPlane") invalid_plane.AttachmentSupport = [(source_box, "Face999")] invalid_plane.MapMode = "FlatFace" document.recompute() invalid_support = { "typeId": invalid_plane.TypeId, "support": link_sub_values(invalid_plane.AttachmentSupport), "state": [str(value) for value in invalid_plane.State], "status": str(invalid_plane.getStatusString()), } with tempfile.TemporaryDirectory(prefix="freecad-partdesign-structure-") as temp_dir: path = os.path.join(temp_dir, "PartDesignStructureOracle.FCStd") document.recompute() document.saveAs(path) App.closeDocument(document.Name) reopened_document = App.openDocument(path) reopened_document.recompute() reopened_source = reopened_document.getObject("SourceBox") reopened_plane = reopened_document.getObject("DatumPlane") reopened_shape_binder = reopened_document.getObject("ShapeBinder") reopened_sub_binder = reopened_document.getObject("SubShapeBinder") reopened_invalid = reopened_document.getObject("InvalidDatumPlane") reopened_bodies = [reopened_document.getObject(name) for name in ["SourceBody", "DatumBody", "BinderBody", "SubBinderBody"]] roundtrip = { "sourceVolume": float(reopened_source.Shape.Volume), "shapeBinderVolume": float(reopened_shape_binder.Shape.Volume), "subShapeBinderLength": float(reopened_sub_binder.Shape.Length), "shapeBinderSupport": link_sub_values(reopened_shape_binder.Support), "subShapeBinderSupport": link_sub_values(reopened_sub_binder.Support), "datumPlaneSupport": link_sub_values(reopened_plane.AttachmentSupport), "datumPlaneMapMode": reopened_plane.MapMode, "datumPlaneAttachmentOffset": placement_json(reopened_plane.AttachmentOffset), "bodyTips": {body.Name: body.Tip.Name if body.Tip else None for body in reopened_bodies}, "bodyGroups": {body.Name: names(body.Group) for body in reopened_bodies}, "invalidDatumState": [str(value) for value in reopened_invalid.State], "invalidDatumStatus": str(reopened_invalid.getStatusString()), } App.closeDocument(reopened_document.Name) with tempfile.TemporaryDirectory(prefix="freecad-partdesign-cross-document-") as temp_dir: source_path = os.path.join(temp_dir, "PartDesignExternalSource.FCStd") consumer_path = os.path.join(temp_dir, "PartDesignExternalConsumer.FCStd") external_source = App.newDocument("PartDesignExternalSource") external_source_body = external_source.addObject("PartDesign::Body", "ExternalSourceBody") external_box = external_source_body.newObject("PartDesign::AdditiveBox", "ExternalBox") external_box.Length = 4 external_box.Width = 5 external_box.Height = 6 external_source.recompute() external_source.saveAs(source_path) external_consumer = App.newDocument("PartDesignExternalConsumer") external_consumer.saveAs(consumer_path) external_shape_body = external_consumer.addObject("PartDesign::Body", "ExternalShapeBody") external_shape_binder = external_shape_body.newObject("PartDesign::ShapeBinder", "ExternalShapeBinder") try: external_shape_binder.Support = [(external_box, "")] shape_binder_external_link = {"accepted": True, "errorType": None, "error": None} except Exception as error: shape_binder_external_link = {"accepted": False, "errorType": type(error).__name__, "error": str(error)} external_sub_body = external_consumer.addObject("PartDesign::Body", "ExternalSubBody") external_sub_binder = external_sub_body.newObject("PartDesign::SubShapeBinder", "ExternalSubShapeBinder") external_sub_binder.Support = [(external_box, ("Face1",))] external_consumer.recompute() cross_document_initial = { "sourceVolume": float(external_box.Shape.Volume), "shapeBinderExternalLink": shape_binder_external_link, "shapeBinderSupportType": external_shape_binder.getTypeIdOfProperty("Support"), "subShapeBinderSupportType": external_sub_binder.getTypeIdOfProperty("Support"), "shapeBinderShapeNull": bool(external_shape_binder.Shape.isNull()), "subShapeBinderArea": float(external_sub_binder.Shape.Area), "sourceFaceArea": float(external_box.Shape.Face1.Area), "shapeBinderSupport": link_sub_values(external_shape_binder.Support), "subShapeBinderSupport": link_sub_values(external_sub_binder.Support), "subShapeBinderSourceDocument": external_sub_binder.Support[0][0].Document.Name, "shapeBodyTip": external_shape_body.Tip.Name if external_shape_body.Tip else None, "subBodyTip": external_sub_body.Tip.Name if external_sub_body.Tip else None, } external_source.save() external_consumer.save() App.closeDocument(external_consumer.Name) App.closeDocument(external_source.Name) reopened_source = App.openDocument(source_path) reopened_consumer = App.openDocument(consumer_path) reopened_source.recompute() reopened_consumer.recompute() reopened_box = reopened_source.getObject("ExternalBox") reopened_shape_body = reopened_consumer.getObject("ExternalShapeBody") reopened_sub_body = reopened_consumer.getObject("ExternalSubBody") reopened_shape_binder = reopened_consumer.getObject("ExternalShapeBinder") reopened_sub_binder = reopened_consumer.getObject("ExternalSubShapeBinder") cross_document_roundtrip = { "sourceVolume": float(reopened_box.Shape.Volume), "shapeBinderShapeNull": bool(reopened_shape_binder.Shape.isNull()), "subShapeBinderArea": float(reopened_sub_binder.Shape.Area), "sourceFaceArea": float(reopened_box.Shape.Face1.Area), "shapeBinderSupport": link_sub_values(reopened_shape_binder.Support), "subShapeBinderSupport": link_sub_values(reopened_sub_binder.Support), "subShapeBinderSourceDocument": reopened_sub_binder.Support[0][0].Document.Name, "shapeBodyTip": reopened_shape_body.Tip.Name if reopened_shape_body.Tip else None, "subBodyTip": reopened_sub_body.Tip.Name if reopened_sub_body.Tip else None, } reopened_box.Width = 8 reopened_source.recompute() reopened_consumer.recompute() cross_document_edited = { "sourceVolume": float(reopened_box.Shape.Volume), "shapeBinderShapeNull": bool(reopened_shape_binder.Shape.isNull()), "subShapeBinderArea": float(reopened_sub_binder.Shape.Area), "sourceFaceArea": float(reopened_box.Shape.Face1.Area), "shapeBinderState": [str(value) for value in reopened_shape_binder.State], "subShapeBinderState": [str(value) for value in reopened_sub_binder.State], } reopened_source.removeObject("ExternalBox") reopened_source.recompute() reopened_consumer.recompute() cross_document_deleted = { "shapeBinderSupport": link_sub_values(reopened_shape_binder.Support), "subShapeBinderSupport": link_sub_values(reopened_sub_binder.Support), "shapeBinderShapeNull": bool(reopened_shape_binder.Shape.isNull()), "subShapeBinderShapeNull": bool(reopened_sub_binder.Shape.isNull()), "subShapeBinderCachedArea": float(reopened_sub_binder.Shape.Area), "shapeBinderState": [str(value) for value in reopened_shape_binder.State], "subShapeBinderState": [str(value) for value in reopened_sub_binder.State], "shapeBinderStatus": str(reopened_shape_binder.getStatusString()), "subShapeBinderStatus": str(reopened_sub_binder.getStatusString()), } App.closeDocument(reopened_consumer.Name) App.closeDocument(reopened_source.Name) report = { "schemaVersion": 1, "baselineId": "freecad-1.1.1-partdesign-structure-oracle", "freecadVersion": version_text(), "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", "status": "pass", "tolerance": 1e-7, "initial": initial, "edited": edited, "invalidSupport": invalid_support, "roundtrip": roundtrip, "crossDocument": { "initial": cross_document_initial, "roundtrip": cross_document_roundtrip, "edited": cross_document_edited, "deleted": cross_document_deleted, }, } print("FREECAD_PARTDESIGN_STRUCTURE_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))