feat: extend FreeCAD parity queue and pair diagnostics
This commit is contained in:
@@ -14,12 +14,11 @@ def version_text():
|
||||
return ".".join(str(value) for value in App.Version()[:3])
|
||||
|
||||
|
||||
def shape_snapshot(feature):
|
||||
shape = feature.Shape
|
||||
def shape_snapshot_from_shape(shape, include_brep=False):
|
||||
if shape.isNull():
|
||||
raise RuntimeError("ordered pair produced a null Shape")
|
||||
bounds = shape.BoundBox
|
||||
return {
|
||||
snapshot = {
|
||||
"valid": bool(shape.isValid()),
|
||||
"solids": len(shape.Solids),
|
||||
"faces": len(shape.Faces),
|
||||
@@ -29,6 +28,15 @@ def shape_snapshot(feature):
|
||||
"area": round(float(shape.Area), 7),
|
||||
"bounds": [round(float(value), 7) for value in (bounds.XMin, bounds.YMin, bounds.ZMin, bounds.XMax, bounds.YMax, bounds.ZMax)],
|
||||
}
|
||||
if include_brep:
|
||||
brep = shape.exportBrepToString()
|
||||
brep_bytes = brep.encode("utf-8") if isinstance(brep, str) else bytes(brep)
|
||||
snapshot["brepSha256"] = hashlib.sha256(brep_bytes).hexdigest()
|
||||
return snapshot
|
||||
|
||||
|
||||
def shape_snapshot(feature):
|
||||
return shape_snapshot_from_shape(feature.Shape)
|
||||
|
||||
|
||||
def phase_snapshot(document):
|
||||
@@ -44,7 +52,35 @@ def phase_snapshot(document):
|
||||
}
|
||||
|
||||
|
||||
def collect():
|
||||
def document_snapshot(document):
|
||||
return {
|
||||
"objects": [{
|
||||
"name": obj.Name,
|
||||
"typeId": obj.TypeId,
|
||||
**({"shape": shape_snapshot(obj)} if hasattr(obj, "Shape") and not obj.Shape.isNull() else {}),
|
||||
} for obj in document.Objects],
|
||||
}
|
||||
|
||||
|
||||
def object_set_snapshot(document):
|
||||
return [{"name": obj.Name, "typeId": obj.TypeId} for obj in document.Objects]
|
||||
|
||||
|
||||
def link_snapshot(value):
|
||||
linked = value
|
||||
subelements = []
|
||||
if isinstance(value, tuple):
|
||||
linked = value[0]
|
||||
if len(value) > 1:
|
||||
subelements = [str(item) for item in value[1]]
|
||||
return {
|
||||
"name": linked.Name if linked is not None else None,
|
||||
"typeId": linked.TypeId if linked is not None else None,
|
||||
"subelements": subelements,
|
||||
}
|
||||
|
||||
|
||||
def collect_accepted():
|
||||
step_path = os.environ["FREECAD_PAIR_STEP_PATH"]
|
||||
evidence_path = os.environ["FREECAD_PAIR_EVIDENCE_PATH"]
|
||||
pair = os.environ["FREECAD_ORDERED_PAIR"]
|
||||
@@ -85,6 +121,7 @@ def collect():
|
||||
"freecadVersion": version_text(),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"pair": pair,
|
||||
"decision": "accepted",
|
||||
"phases": {"initial": initial, "reopened": reopened_snapshot, "resaved": resaved_snapshot},
|
||||
"checks": {
|
||||
"shapeStable": initial["shape"] == reopened_snapshot["shape"] and initial["shape"] == resaved_snapshot["shape"],
|
||||
@@ -97,8 +134,162 @@ def collect():
|
||||
App.closeDocument(name)
|
||||
|
||||
|
||||
def collect_rejected():
|
||||
step_path = os.environ["FREECAD_PAIR_STEP_PATH"]
|
||||
pair = os.environ["FREECAD_ORDERED_PAIR"]
|
||||
operation = pair.split("->", 1)[1]
|
||||
output_directory = os.environ["FREECAD_PAIR_OUTPUT_DIRECTORY"]
|
||||
document = App.newDocument("FreeCadOrderedPairRejection")
|
||||
try:
|
||||
Import.insert(step_path, document.Name)
|
||||
document.recompute()
|
||||
source_objects = [obj for obj in document.Objects if hasattr(obj, "Shape") and not obj.Shape.isNull()]
|
||||
if len(source_objects) != 1:
|
||||
raise RuntimeError("Fuse STEP import must produce exactly one Shape object")
|
||||
source = source_objects[0]
|
||||
if len(source.Shape.Solids) != 1:
|
||||
raise RuntimeError("Fuse STEP import did not produce one Solid")
|
||||
if source.Shape.isNull():
|
||||
raise RuntimeError("STEP import produced no Shape")
|
||||
source_name = source.Name
|
||||
source_type_id = source.TypeId
|
||||
document.UndoMode = 1
|
||||
baseline = document_snapshot(document)
|
||||
baseline_objects = object_set_snapshot(document)
|
||||
baseline_source_shape = shape_snapshot_from_shape(source.Shape, include_brep=True)
|
||||
diagnostic = {
|
||||
"exceptionType": None,
|
||||
"exception": None,
|
||||
"recomputeResult": None,
|
||||
"statusString": None,
|
||||
"state": [],
|
||||
"shapeNull": True,
|
||||
"shapeValid": False,
|
||||
}
|
||||
document.openTransaction("ordered-pair-rejection")
|
||||
try:
|
||||
if operation == "pad":
|
||||
body = document.addObject("PartDesign::Body", "Body")
|
||||
feature = body.newObject("PartDesign::Pad", "PairResult")
|
||||
feature.Profile = source
|
||||
feature.Length = 5.0
|
||||
diagnostic["parameter"] = {"name": "length", "value": round(float(feature.Length), 7)}
|
||||
diagnostic["input"] = link_snapshot(feature.Profile)
|
||||
elif operation == "loft":
|
||||
section = document.addObject("Part::Feature", "Section")
|
||||
section.Shape = Part.makePolygon([
|
||||
App.Vector(0.0, 0.0, 15.0),
|
||||
App.Vector(2.0, 0.0, 15.0),
|
||||
App.Vector(2.0, 3.0, 15.0),
|
||||
App.Vector(0.0, 3.0, 15.0),
|
||||
App.Vector(0.0, 0.0, 15.0),
|
||||
])
|
||||
feature = document.addObject("Part::Loft", "PairResult")
|
||||
feature.Sections = [source, section]
|
||||
feature.Solid = True
|
||||
feature.Ruled = False
|
||||
diagnostic["parameter"] = {"name": "ruled", "value": bool(feature.Ruled)}
|
||||
diagnostic["input"] = {
|
||||
"sections": [{"name": item.Name, "typeId": item.TypeId} for item in feature.Sections],
|
||||
}
|
||||
elif operation == "pipe":
|
||||
spine = document.addObject("Part::Feature", "Spine")
|
||||
spine.Shape = Part.makePolygon([App.Vector(0.0, 0.0, 0.0), App.Vector(0.0, 0.0, 15.0)])
|
||||
feature = document.addObject("Part::Sweep", "PairResult")
|
||||
feature.Sections = [source]
|
||||
feature.Spine = spine
|
||||
feature.Solid = True
|
||||
feature.Frenet = False
|
||||
diagnostic["parameter"] = {"name": "spineLength", "value": 15.0}
|
||||
diagnostic["input"] = {
|
||||
"sections": [{"name": item.Name, "typeId": item.TypeId} for item in feature.Sections],
|
||||
"spine": link_snapshot(feature.Spine),
|
||||
}
|
||||
else:
|
||||
raise RuntimeError("Unsupported rejected ordered-pair operation: " + operation)
|
||||
diagnostic["nativeTypeId"] = feature.TypeId
|
||||
diagnostic["inputShape"] = shape_snapshot_from_shape(source.Shape)
|
||||
diagnostic["objects"] = object_set_snapshot(document)
|
||||
try:
|
||||
diagnostic["recomputeResult"] = bool(document.recompute())
|
||||
except Exception as error:
|
||||
diagnostic["exceptionType"] = type(error).__name__
|
||||
diagnostic["exception"] = str(error)
|
||||
diagnostic["statusString"] = str(feature.getStatusString())
|
||||
diagnostic["state"] = [str(value) for value in feature.State]
|
||||
diagnostic["shapeNull"] = bool(feature.Shape.isNull())
|
||||
diagnostic["shapeValid"] = False if feature.Shape.isNull() else bool(feature.Shape.isValid())
|
||||
finally:
|
||||
document.abortTransaction()
|
||||
document.recompute()
|
||||
initial = document_snapshot(document)
|
||||
restored_objects = object_set_snapshot(document)
|
||||
restored_source = document.getObject(source_name)
|
||||
if restored_source is None or not hasattr(restored_source, "Shape") or restored_source.Shape.isNull():
|
||||
raise RuntimeError("Fuse source Shape is absent after transaction abort")
|
||||
restored_source_shape = shape_snapshot_from_shape(restored_source.Shape, include_brep=True)
|
||||
initial_path = os.path.join(output_directory, "ordered-pair-rejection.FCStd")
|
||||
resaved_path = os.path.join(output_directory, "ordered-pair-rejection-resaved.FCStd")
|
||||
document.saveAs(initial_path)
|
||||
App.closeDocument(document.Name)
|
||||
reopened = App.openDocument(initial_path)
|
||||
reopened.recompute()
|
||||
reopened_snapshot = document_snapshot(reopened)
|
||||
reopened.saveAs(resaved_path)
|
||||
App.closeDocument(reopened.Name)
|
||||
resaved = App.openDocument(resaved_path)
|
||||
resaved.recompute()
|
||||
resaved_snapshot = document_snapshot(resaved)
|
||||
App.closeDocument(resaved.Name)
|
||||
feature_rejected = diagnostic["shapeNull"] and (
|
||||
diagnostic["exceptionType"] is not None
|
||||
or diagnostic["recomputeResult"] is False
|
||||
or diagnostic["statusString"] != "Valid"
|
||||
or "Error" in diagnostic["state"]
|
||||
or "Invalid" in diagnostic["state"]
|
||||
)
|
||||
expected_type_id = {"pad": "PartDesign::Pad", "loft": "Part::Loft", "pipe": "Part::Sweep"}[operation]
|
||||
input_names = [diagnostic.get("input", {}).get("name")] if operation == "pad" else [item["name"] for item in diagnostic.get("input", {}).get("sections", [])]
|
||||
checks = {
|
||||
"nativeFeatureConstructed": diagnostic.get("nativeTypeId") == expected_type_id,
|
||||
"fuseSolidUsedAsInput": source_name in input_names and diagnostic.get("inputShape", {}).get("solids") == 1,
|
||||
"recomputeStatusCaptured": diagnostic["recomputeResult"] is not None or diagnostic["exceptionType"] is not None,
|
||||
"nativeFeatureRejected": feature_rejected,
|
||||
"objectSetRestored": baseline_objects == restored_objects,
|
||||
"sourceShapeRestored": baseline_source_shape == restored_source_shape,
|
||||
"documentUnpolluted": baseline == initial,
|
||||
"documentStable": initial == reopened_snapshot and initial == resaved_snapshot,
|
||||
}
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"baselineId": "freecad-1.1.1-ordered-operation-pair-resave",
|
||||
"freecadVersion": version_text(),
|
||||
"gitCommit": FREECAD_COMMIT,
|
||||
"pair": pair,
|
||||
"decision": "rejected",
|
||||
"operation": operation,
|
||||
"nativeTypeId": expected_type_id,
|
||||
"source": {
|
||||
"name": source_name,
|
||||
"typeId": source_type_id,
|
||||
"shape": baseline_source_shape,
|
||||
},
|
||||
"diagnostic": diagnostic,
|
||||
"rollback": {
|
||||
"before": {"objects": baseline_objects, "sourceShape": baseline_source_shape},
|
||||
"after": {"objects": restored_objects, "sourceShape": restored_source_shape},
|
||||
},
|
||||
"phases": {"initial": initial, "reopened": reopened_snapshot, "resaved": resaved_snapshot},
|
||||
"checks": checks,
|
||||
"status": "pass" if all(checks.values()) else "failed",
|
||||
}
|
||||
finally:
|
||||
for name in list(App.listDocuments().keys()):
|
||||
App.closeDocument(name)
|
||||
|
||||
|
||||
try:
|
||||
report = collect()
|
||||
report = collect_rejected() if os.environ.get("FREECAD_PAIR_DECISION") == "rejected" else collect_accepted()
|
||||
except Exception as error:
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
|
||||
Reference in New Issue
Block a user