import json import FreeCAD as App import Part import Sketcher from freecad_mutation_evidence import capture_property_mutation def version_text(): return ".".join(str(value) for value in App.Version()[:3]) def shape_result(case_id, document, feature): document.recompute() shape = feature.Shape return { "id": case_id, "typeId": feature.TypeId, "passed": not shape.isNull() and shape.isValid() and len(shape.Solids) == 1, "status": feature.getStatusString(), "solids": len(shape.Solids) if not shape.isNull() else 0, "faces": len(shape.Faces) if not shape.isNull() else 0, "edges": len(shape.Edges) if not shape.isNull() else 0, "volume": float(shape.Volume) if not shape.isNull() else 0.0, } def add_rectangle(sketch, x_min, y_min, x_max, y_max): points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)] for index, start in enumerate(points): end = points[(index + 1) % len(points)] sketch.addGeometry(Part.LineSegment(App.Vector(start[0], start[1], 0), App.Vector(end[0], end[1], 0)), False) def feature_linear_pattern(): document = App.newDocument("FeatureLinearPatternOracle") body = document.addObject("PartDesign::Body", "Body") base = body.newObject("PartDesign::AdditiveBox", "Base") base.Length = 20 base.Width = 10 base.Height = 2 boss = body.newObject("PartDesign::AdditiveBox", "Boss") boss.Length = 2 boss.Width = 2 boss.Height = 2 boss.Placement.Base = App.Vector(2, 4, 2) document.recompute() pattern = body.newObject("PartDesign::LinearPattern", "LinearPattern") pattern.TransformMode = "Features" pattern.Originals = [boss] pattern.Direction = (document.X_Axis, [""]) pattern.Occurrences = 3 pattern.Length = 12 mutation = capture_property_mutation(document, pattern, "Occurrences", 4) result = shape_result("linear-feature-list", document, pattern) result["mutation"] = mutation result.update({ "transformMode": pattern.TransformMode, "originals": [item.Name for item in pattern.Originals], "direction": pattern.Direction[0].Name, "occurrences": int(pattern.Occurrences), "length": float(pattern.Length), }) App.closeDocument(document.Name) return result def feature_mirrored(): document = App.newDocument("FeatureMirroredOracle") body = document.addObject("PartDesign::Body", "Body") base = body.newObject("PartDesign::AdditiveBox", "Base") base.Length = 20 base.Width = 10 base.Height = 2 boss = body.newObject("PartDesign::AdditiveBox", "Boss") boss.Length = 2 boss.Width = 2 boss.Height = 2 boss.Placement.Base = App.Vector(0, 4, 2) document.recompute() mirrored = body.newObject("PartDesign::Mirrored", "Mirrored") mirrored.TransformMode = "Features" mirrored.Originals = [boss] mirrored.MirrorPlane = (document.YZ_Plane, [""]) mutation = capture_property_mutation(document, mirrored, "Refine", False) result = shape_result("mirrored-feature-list", document, mirrored) result["mutation"] = mutation result.update({ "transformMode": mirrored.TransformMode, "originals": [item.Name for item in mirrored.Originals], "plane": mirrored.MirrorPlane[0].Name, }) App.closeDocument(document.Name) return result def feature_polar_pattern(): document = App.newDocument("FeaturePolarPatternOracle") body = document.addObject("PartDesign::Body", "Body") box = document.addObject("PartDesign::AdditiveBox", "Box") body.addObject(box) box.Length = 10 box.Width = 10 box.Height = 10 document.recompute() pattern = document.addObject("PartDesign::PolarPattern", "PolarPattern") pattern.Originals = [box] pattern.Axis = (document.X_Axis, [""]) pattern.Angle = 360 pattern.Occurrences = 4 pattern.Refine = True body.addObject(pattern) mutation = capture_property_mutation(document, pattern, "Occurrences", 3) result = shape_result("polar-feature-list", document, pattern) result["mutation"] = mutation result.update({ "transformMode": pattern.TransformMode, "originals": [item.Name for item in pattern.Originals], "axis": pattern.Axis[0].Name, "angle": float(pattern.Angle), "occurrences": int(pattern.Occurrences), }) App.closeDocument(document.Name) return result def feature_multi_transform(): document = App.newDocument("FeatureMultiTransformOracle") body = document.addObject("PartDesign::Body", "Body") sketch = document.addObject("Sketcher::SketchObject", "SketchPad") body.addObject(sketch) add_rectangle(sketch, 0, 0, 10, 10) document.recompute() pad = document.addObject("PartDesign::Pad", "Pad") body.addObject(pad) pad.Profile = sketch pad.Length = 10 document.recompute() multi = document.addObject("PartDesign::MultiTransform", "MultiTransform") document.recompute() multi.Originals = [pad] multi.Shape = pad.Shape body.addObject(multi) document.recompute() mirrored = document.addObject("PartDesign::Mirrored", "Mirrored") mirrored.MirrorPlane = (sketch, ["H_Axis"]) body.addObject(mirrored) linear = document.addObject("PartDesign::LinearPattern", "LinearPattern") linear.Direction = (sketch, ["H_Axis"]) linear.Length = 20 linear.Occurrences = 3 body.addObject(linear) polar = document.addObject("PartDesign::PolarPattern", "PolarPattern") polar.Axis = (sketch, ["N_Axis"]) polar.Angle = 360 polar.Occurrences = 4 body.addObject(polar) multi.Transformations = [mirrored, linear, polar] mutation = capture_property_mutation(document, multi, "Length", 15.0, target=linear, property_path="Transformations[1].Length") result = shape_result("multi-transform-feature-list", document, multi) result["mutation"] = mutation result.update({ "transformMode": multi.TransformMode, "originals": [item.Name for item in multi.Originals], "transformations": [item.TypeId for item in multi.Transformations], }) App.closeDocument(document.Name) return result def transform_failure(type_id): suffix = type_id.split("::", 1)[1] document = App.newDocument("Feature{}FailureOracle".format(suffix)) try: body = document.addObject("PartDesign::Body", "Body") base = body.newObject("PartDesign::AdditiveBox", "Base") base.Length = 20 base.Width = 10 base.Height = 2 boss = body.newObject("PartDesign::AdditiveBox", "Boss") boss.Length = 2 boss.Width = 2 boss.Height = 2 boss.Placement.Base = App.Vector(2, 4, 2) document.recompute() feature = document.addObject(type_id, suffix) feature.Originals = [boss] if type_id == "PartDesign::MultiTransform": feature.Shape = boss.Shape body.addObject(feature) if type_id == "PartDesign::LinearPattern": feature.Direction = (base, ["Edge999"]) if type_id == "PartDesign::MultiTransform": child = document.addObject("PartDesign::LinearPattern", "InvalidLinearChild") child.Length = 12 child.Occurrences = 3 child.Direction = (base, ["Edge999"]) body.addObject(child) feature.Transformations = [child] document.recompute() shape = feature.Shape shape_null = bool(shape.isNull()) state = [str(value) for value in feature.State] status_string = str(feature.getStatusString()) invalid = "Invalid" in state or status_string != "Valid" observed = "rejected" if invalid else "accepted-empty" if shape_null else "accepted" return { "id": "{}-missing-reference".format(suffix.lower()), "typeId": type_id, "inputClass": "valid-original-missing-transform-reference", "expected": "rejected", "observed": observed, "passed": observed == "rejected", "shapeNull": shape_null, "staleShapePreserved": bool(invalid and not shape_null), "state": state, "status": status_string, "originals": [item.Name for item in feature.Originals], "transformations": [item.TypeId for item in feature.Transformations] if type_id == "PartDesign::MultiTransform" else [], } except Exception as error: return { "id": "{}-missing-reference".format(suffix.lower()), "typeId": type_id, "inputClass": "valid-original-missing-transform-reference", "expected": "rejected", "observed": "rejected", "passed": True, "errorType": type(error).__name__, "error": str(error), } finally: App.closeDocument(document.Name) def hole_document(name): document = App.newDocument(name) body = document.addObject("PartDesign::Body", "Body") box = body.newObject("PartDesign::AdditiveBox", "Box") box.Length = 20 box.Width = 20 box.Height = 10 document.recompute() top_face = max(range(len(box.Shape.Faces)), key=lambda index: box.Shape.Faces[index].CenterOfMass.z) sketch = body.newObject("Sketcher::SketchObject", "HoleSketch") sketch.AttachmentSupport = (box, ["Face{}".format(top_face + 1)]) sketch.MapMode = "FlatFace" sketch.addGeometry(Part.Circle(App.Vector(10, 10, 0), App.Vector(0, 0, 1), 3), False) hole = body.newObject("PartDesign::Hole", "Hole") hole.Profile = sketch hole.DepthType = "Dimension" hole.Depth = 8 hole.DrillPoint = "Flat" return document, body, hole def iso_hole(modeled): case_id = "iso-m6-modeled-thread" if modeled else "iso-m6-tap-drill" document, body, hole = hole_document("ModeledThreadOracle" if modeled else "TapDrillOracle") hole.ThreadType = "ISOMetricProfile" hole.ThreadSize = "M6x1.0" hole.Threaded = True hole.ModelThread = modeled hole.ThreadDirection = "Left" if modeled else "Right" hole.ThreadDepthType = "Dimension" hole.ThreadDepth = 6 mutation = capture_property_mutation(document, hole, "Diameter", 6.5) result = shape_result(case_id, document, hole) result["mutation"] = mutation result.update({ "threaded": bool(hole.Threaded), "modeled": bool(hole.ModelThread), "threadType": hole.ThreadType, "threadSize": hole.ThreadSize, "threadClass": hole.ThreadClass, "threadFit": hole.ThreadFit, "threadDirection": hole.ThreadDirection, "threadDepthType": hole.ThreadDepthType, "threadDepth": float(hole.ThreadDepth), "diameter": float(hole.Diameter), "threadDiameter": float(hole.ThreadDiameter), "threadPitch": float(hole.ThreadPitch) if "ThreadPitch" in hole.PropertiesList else 1.0, "properties": list(hole.PropertiesList), }) App.closeDocument(document.Name) return result success_cases = [feature_linear_pattern(), feature_mirrored(), feature_polar_pattern(), feature_multi_transform(), iso_hole(False), iso_hole(True)] failure_cases = [transform_failure(type_id) for type_id in [ "PartDesign::Mirrored", "PartDesign::MultiTransform", "PartDesign::LinearPattern", "PartDesign::PolarPattern", ]] cases = success_cases + failure_cases report = { "schemaVersion": 1, "baselineId": "freecad-1.1.1-partdesign-transform-oracle", "freecadVersion": version_text(), "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d", "status": "pass" if all(case["passed"] for case in cases) else "failed", "tolerance": 1e-7, "cases": cases, "summary": { "cases": len(cases), "passed": sum(1 for case in cases if case["passed"]), "successCases": len(success_cases), "successPassed": sum(1 for case in success_cases if case["passed"]), "failureCases": len(failure_cases), "failurePassed": sum(1 for case in failure_cases if case["passed"]), "rejected": sum(1 for case in failure_cases if case.get("observed") == "rejected"), "acceptedEmpty": sum(1 for case in failure_cases if case.get("observed") == "accepted-empty"), }, } print("FREECAD_PARTDESIGN_TRANSFORM_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))