215 lines
8.7 KiB
Python
215 lines
8.7 KiB
Python
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=(",", ":")))
|