Files
Web_FreeCAD_Bitbybit/scripts/freecad-attachment-combination-oracle.py
wangdequan f64b78865c
Some checks failed
real-verification / chrome (push) Has been cancelled
real-verification / freecad-oracle (push) Has been cancelled
real-verification / wasm (push) Has been cancelled
feat: complete production naming and reference lifecycle gates
2026-08-14 10:10:30 -04:00

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=(",", ":")))