import json import math import os import tempfile import FreeCAD as App import Part import Sketcher # noqa: F401 - registers Sketcher::SketchObject FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d" TRANSLATION = App.Vector(1.25, -0.75, 2.0) def version_text(): return ".".join(str(value) for value in App.Version()[:3]) 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 / math.pi), } def support_json(value): result = [] for obj, sub_elements in value: subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements) result.append({ "object": obj.Name, "subElements": [str(sub) for sub in subs if str(sub)], }) return result def finite_placement(value): return all(math.isfinite(number) for number in value["position"] + value["axis"] + [value["angleDegrees"]]) def add_shape(document, name, shape): obj = document.addObject("PartDesign::Feature", name) obj.Shape = shape return obj def make_sources(document): vector = App.Vector sources = { "BoxA": add_shape(document, "BoxA", Part.makeBox(4, 5, 6, vector(0, 0, 0))), "BoxB": add_shape(document, "BoxB", Part.makeBox(3, 4, 7, vector(12, 3, 2))), "BoxC": add_shape(document, "BoxC", Part.makeBox(2, 6, 4, vector(-8, 4, 1))), "BoxD": add_shape(document, "BoxD", Part.makeBox(5, 3, 2, vector(3, -9, 5))), "PlaneXY": add_shape(document, "PlaneXY", Part.makePlane(20, 20, vector(-10, -10, 0), vector(0, 0, 1))), "PlaneYZ": add_shape(document, "PlaneYZ", Part.makePlane(20, 20, vector(0, -10, -10), vector(1, 0, 0))), "Circle": add_shape(document, "Circle", Part.Circle(vector(0, 0, 0), vector(0, 0, 1), 5).toShape()), "Ellipse": add_shape(document, "Ellipse", Part.Ellipse(vector(0, 0, 0), 6, 3).toShape()), "Hyperbola": add_shape(document, "Hyperbola", Part.Hyperbola(vector(0, 0, 0), 5, 3).toShape(-1.0, 1.0)), "Parabola": add_shape(document, "Parabola", Part.Parabola().toShape(-2.0, 2.0)), "Curve": add_shape(document, "Curve", Part.Arc(vector(-4, 0, 0), vector(0, 4, 2), vector(4, 0, 0)).toShape()), "V0": add_shape(document, "V0", Part.Vertex(vector(0, 0, 0))), "V1": add_shape(document, "V1", Part.Vertex(vector(5, 0, 0))), "V2": add_shape(document, "V2", Part.Vertex(vector(0, 4, 2))), "V3": add_shape(document, "V3", Part.Vertex(vector(1, 2, 6))), "LX": add_shape(document, "LX", Part.makeLine(vector(0, 0, 0), vector(6, 0, 0))), "LY": add_shape(document, "LY", Part.makeLine(vector(0, 0, 0), vector(0, 5, 0))), "LZ": add_shape(document, "LZ", Part.makeLine(vector(0, 0, 0), vector(0, 0, 4))), "LD": add_shape(document, "LD", Part.makeLine(vector(0, 0, 0), vector(3, 4, 5))), "LOff": add_shape(document, "LOff", Part.makeLine(vector(0, 4, 2), vector(3, 5, 6))), } document.recompute() return sources def source_ref(sources, reference_type, index, combination): if reference_type == "Vertex": if combination == ["Face", "Vertex"] or combination == ["Vertex", "Face"]: name = "V1" elif any(value in ("Curve", "Circle", "Edge") for value in combination): name = "V1" elif combination in (["Line", "Vertex"], ["Vertex", "Line"]): name = "V2" else: name = ["V0", "V1", "V2", "V3"][index % 4] return sources[name], "Vertex1" if reference_type == "Line": if combination == ["Line", "Line"] and index == 1: return sources["LOff"], "Edge1" return sources[["LX", "LY", "LZ", "LD"][index % 4]], "Edge1" if reference_type == "Plane|Placement": return sources["PlaneXY" if index == 0 else "PlaneYZ"], "" if reference_type in ("Plane", "Face"): return sources["PlaneXY" if index == 0 else "PlaneYZ"], "Face1" if reference_type == "Edge": return sources["Circle"], "Edge1" if reference_type == "Curve": return sources["Curve"], "Edge1" if reference_type == "Circle": return sources["Circle"], "Edge1" if reference_type == "Conic": return sources["Ellipse"], "Edge1" if reference_type == "Ellipse": return sources["Ellipse"], "Edge1" if reference_type == "Hyperbola": return sources["Hyperbola"], "Edge1" if reference_type in ("Any", "Any|Placement"): return sources[["BoxA", "BoxB", "BoxC", "BoxD"][index % 4]], "" raise ValueError("Unsupported attachment reference category: " + reference_type) def supports_for(sources, combination): return [source_ref(sources, reference_type, index, combination) for index, reference_type in enumerate(combination)] def case_state(obj): suggestion = obj.Attacher.suggestModes() placement = placement_json(obj.Placement) return { "mapMode": str(obj.MapMode), "support": support_json(obj.AttachmentSupport), "placement": placement, "finitePlacement": finite_placement(placement), "state": [str(value) for value in obj.State], "status": str(obj.getStatusString()), "positionBySupport": bool(obj.positionBySupport()), "suggestedModes": list(suggestion["allApplicableModes"]), "suggestionMessage": suggestion["message"], } document = App.newDocument("AttachmentCombinationOracle") sources = make_sources(document) engine_specs = { "plane": ("PartDesign::Plane", document.addObject("PartDesign::Body", "PlaneCases")), "line": ("PartDesign::Line", document.addObject("PartDesign::Body", "LineCases")), "point": ("PartDesign::Point", document.addObject("PartDesign::Body", "PointCases")), "sketch": ("Sketcher::SketchObject", document.addObject("PartDesign::Body", "SketchCases")), } seed_objects = { name: body.newObject(type_id, "Seed" + name.title()) for name, (type_id, body) in engine_specs.items() } document.recompute() cases = [] case_objects = {} for engine_name, seed in seed_objects.items(): type_id, body = engine_specs[engine_name] for mode in list(seed.Attacher.ImplementedModes): combinations = seed.Attacher.getModeInfo(mode)["ReferenceCombinations"] for combination_index, combination_value in enumerate(combinations): combination = [str(value) for value in combination_value] case_id = "%s-%s-%02d" % (engine_name, mode, combination_index) object_name = "Case%04d" % (len(cases) + 1) obj = body.newObject(type_id, object_name) obj.Label = case_id obj.AttachmentSupport = supports_for(sources, combination) obj.MapMode = mode cases.append({ "id": case_id, "objectName": object_name, "engine": engine_name, "typeId": type_id, "mode": str(mode), "modeIndex": int(seed.Attacher.getModeInfo(mode)["ModeIndex"]), "combinationIndex": combination_index, "referenceCombination": combination, }) case_objects[object_name] = obj for seed in seed_objects.values(): seed.Document.removeObject(seed.Name) document.recompute() initial = {name: case_state(obj) for name, obj in case_objects.items()} for source in sources.values(): source.Placement.Base = source.Placement.Base + TRANSLATION document.recompute() mutated = {name: case_state(obj) for name, obj in case_objects.items()} with tempfile.TemporaryDirectory(prefix="freecad-attachment-combinations-") as temp_dir: path = os.path.join(temp_dir, "AttachmentCombinationOracle.FCStd") document.saveAs(path) App.closeDocument(document.Name) reopened = App.openDocument(path) reopened.recompute() roundtrip = {case["objectName"]: case_state(reopened.getObject(case["objectName"])) for case in cases} App.closeDocument(reopened.Name) for case in cases: name = case["objectName"] case["initial"] = initial[name] case["mutated"] = mutated[name] case["roundtrip"] = roundtrip[name] report = { "schemaVersion": 1, "baselineId": "freecad-1.1.1-attachment-combination-oracle", "freecadVersion": version_text(), "gitCommit": FREECAD_COMMIT, "status": "pass", "tolerance": 1e-7, "translation": [float(TRANSLATION.x), float(TRANSLATION.y), float(TRANSLATION.z)], "caseCount": len(cases), "engineCaseCounts": { engine: len([case for case in cases if case["engine"] == engine]) for engine in engine_specs }, "cases": cases, } print("FREECAD_ATTACHMENT_COMBINATION_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))