import hashlib import json import os import FreeCAD as App import Part FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" OUTPUT_PATH = os.environ.get("FREECAD_PROPERTY_DIRECTION_MUTATION_OUTPUT", "") EDITED_VALUE = (0.25, -0.5, 1.5) def vector_value(obj, property_name): value = getattr(obj, property_name) return [float(value.x), float(value.y), float(value.z)] def object_set(document): return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects] def shape_snapshot(obj): shape = obj.Shape if shape.isNull(): return { "isNull": True, "valid": False, "shapeType": "Null", "solids": 0, "faces": 0, "edges": 0, "vertices": 0, } bounds = shape.BoundBox return { "isNull": False, "valid": bool(shape.isValid()), "shapeType": shape.ShapeType, "solids": len(shape.Solids), "faces": len(shape.Faces), "edges": len(shape.Edges), "vertices": len(shape.Vertexes), "area": round(float(shape.Area), 9), "volume": round(float(shape.Volume), 9), "bounds": [round(float(value), 9) for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)], "brepSha256": hashlib.sha256(shape.exportBrepToString().encode("utf-8")).hexdigest(), } def snapshot(document, obj, property_name, recompute_result=None): return { "value": vector_value(obj, property_name), "propertyTypeId": obj.getTypeIdOfProperty(property_name), "propertyStatus": [str(item) for item in obj.getPropertyStatus(property_name)], "editorMode": [str(item) for item in obj.getEditorMode(property_name)], "objectState": [str(item) for item in obj.State], "statusString": str(obj.getStatusString()), "mustExecute": bool(obj.MustExecute), "recomputeResult": recompute_result, "shape": shape_snapshot(obj), "objectSet": object_set(document), } def setup_mirroring(document): source = document.addObject("Part::Box", "MirrorSource") source.Length = 2 source.Width = 3 source.Height = 4 host = document.addObject("Part::Mirroring", "DirectionMirror") host.Source = source host.Base = App.Vector(0, 0, 0) return host, "Normal" def setup_projection(document): support = document.addObject("Part::Feature", "ProjectionSupport") support.Shape = Part.makePlane(40, 40, App.Vector(-20, -20, 0)) wire = document.addObject("Part::Feature", "ProjectionWire") wire.Shape = Part.makePolygon([ App.Vector(-4, -3, -5), App.Vector(4, -3, -5), App.Vector(4, 3, -5), App.Vector(-4, 3, -5), App.Vector(-4, -3, -5), ]) host = document.addObject("Part::ProjectOnSurface", "DirectionProjection") host.SupportFace = (support, ["Face1"]) host.Projection = [(wire, ["Wire1"])] host.Mode = "Edges" return host, "Direction" def run_case(case_id, object_type_id, setup): document = App.newDocument("PropertyDirectionMutation" + case_id) try: obj, property_name = setup(document) before_recompute = bool(document.recompute()) before = snapshot(document, obj, property_name, before_recompute) setattr(obj, property_name, EDITED_VALUE) after_set = snapshot(document, obj, property_name) edited_recompute = bool(document.recompute()) after_recompute = snapshot(document, obj, property_name, edited_recompute) setattr(obj, property_name, tuple(before["value"])) after_restore_set = snapshot(document, obj, property_name) restored_recompute = bool(document.recompute()) after_restore_recompute = snapshot(document, obj, property_name, restored_recompute) return { "caseId": case_id, "objectTypeId": object_type_id, "objectName": obj.Name, "propertyName": property_name, "editedValue": list(EDITED_VALUE), "phases": { "before": before, "afterSet": after_set, "afterRecompute": after_recompute, "afterRestoreSet": after_restore_set, "afterRestoreRecompute": after_restore_recompute, }, "valueChanged": before["value"] != after_recompute["value"], "valueRestored": before["value"] == after_restore_recompute["value"], "geometryChanged": before["shape"]["brepSha256"] != after_recompute["shape"]["brepSha256"], "geometryRestored": before["shape"] == after_restore_recompute["shape"], "objectsPreserved": before["objectSet"] == after_recompute["objectSet"] == after_restore_recompute["objectSet"], } finally: App.closeDocument(document.Name) cases = [ run_case("Mirroring", "Part::Mirroring", setup_mirroring), run_case("ProjectOnSurface", "Part::ProjectOnSurface", setup_projection), ] report = { "schemaVersion": 1, "status": "pass", "baselineId": "freecad-1.1.1-property-direction-mutation", "freecadVersion": ".".join(str(value) for value in App.Version()[:3]), "gitCommit": FREECAD_COMMIT, "propertyType": "App::PropertyDirection", "caseCount": len(cases), "cases": cases, } if not OUTPUT_PATH: raise RuntimeError("FREECAD_PROPERTY_DIRECTION_MUTATION_OUTPUT is required") with open(OUTPUT_PATH, "w", encoding="utf-8") as handle: json.dump(report, handle, indent=2, sort_keys=True) handle.write("\n") print("FREECAD_PROPERTY_DIRECTION_MUTATION_RESULT=" + json.dumps({"status": report["status"], "caseCount": report["caseCount"]}, sort_keys=True))