feat: complete production naming and reference lifecycle gates
This commit is contained in:
214
scripts/freecad-attachment-combination-oracle.py
Normal file
214
scripts/freecad-attachment-combination-oracle.py
Normal file
@@ -0,0 +1,214 @@
|
||||
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=(",", ":")))
|
||||
Reference in New Issue
Block a user