Files
Web_FreeCAD_Bitbybit/scripts/freecad_mutation_evidence.py

87 lines
4.0 KiB
Python

import hashlib
def _json_value(value):
if value is None or isinstance(value, (bool, int, float, str)):
return value
if hasattr(value, "Value") and hasattr(value, "Unit"):
return {"value": float(value.Value), "unit": str(value.Unit)}
if hasattr(value, "x") and hasattr(value, "y") and hasattr(value, "z"):
return [float(value.x), float(value.y), float(value.z)]
if hasattr(value, "Name") and hasattr(value, "TypeId"):
return {"name": str(value.Name), "typeId": str(value.TypeId)}
if isinstance(value, (list, tuple)):
return [_json_value(item) for item in value]
return str(value)
def _shape_snapshot(feature):
shape = feature.Shape
if shape.isNull():
raise RuntimeError("{} produced a null Shape".format(feature.Name))
if not shape.isValid():
raise RuntimeError("{} produced an invalid Shape".format(feature.Name))
brep = shape.exportBrepToString().encode("utf-8")
bounds = shape.BoundBox
return {
"brepSha256": hashlib.sha256(brep).hexdigest(),
"shapeType": str(shape.ShapeType),
"valid": True,
"solids": len(shape.Solids),
"faces": len(shape.Faces),
"edges": len(shape.Edges),
"vertices": len(shape.Vertexes),
"volume": float(shape.Volume),
"area": float(shape.Area),
"bounds": {
"min": [float(bounds.XMin), float(bounds.YMin), float(bounds.ZMin)],
"max": [float(bounds.XMax), float(bounds.YMax), float(bounds.ZMax)],
},
}
def capture_property_mutation(document, feature, property_name, edited_value, target=None, property_path=None):
target = target or feature
original_value = getattr(target, property_name)
before_parameter = _json_value(original_value)
document.recompute()
before_shape_object = feature.Shape.copy()
before_shape = _shape_snapshot(feature)
try:
setattr(target, property_name, edited_value)
document.recompute()
edited_parameter = _json_value(getattr(target, property_name))
if before_parameter == edited_parameter:
raise RuntimeError("{} mutation did not change {}".format(feature.TypeId, property_path or property_name))
edited_shape = _shape_snapshot(feature)
finally:
setattr(target, property_name, original_value)
document.recompute()
restored_shape_object = feature.Shape
restored_shape = _shape_snapshot(feature)
restored_parameter = _json_value(getattr(target, property_name))
if restored_parameter != before_parameter:
raise RuntimeError("{} did not restore {}".format(feature.TypeId, property_path or property_name))
restored_equivalent = bool(restored_shape_object.isEqual(before_shape_object))
geometry_equivalent = (restored_shape["shapeType"], restored_shape["solids"], restored_shape["faces"], restored_shape["edges"], restored_shape["vertices"], round(restored_shape["volume"], 7), round(restored_shape["area"], 7)) == (before_shape["shapeType"], before_shape["solids"], before_shape["faces"], before_shape["edges"], before_shape["vertices"], round(before_shape["volume"], 7), round(before_shape["area"], 7))
if not restored_equivalent and not geometry_equivalent:
raise RuntimeError("{} did not restore the original geometry".format(feature.TypeId))
return {
"schemaVersion": 1,
"familyId": str(feature.TypeId),
"featureName": str(feature.Name),
"propertyPath": property_path or property_name,
"status": "pass",
"before": {"parameterSnapshot": before_parameter, "shape": before_shape},
"edited": {"parameterSnapshot": edited_parameter, "shape": edited_shape},
"restored": {"parameterSnapshot": restored_parameter, "shape": restored_shape},
"parameterChanged": True,
"shapeChanged": edited_shape["brepSha256"] != before_shape["brepSha256"],
"restoredExactly": restored_equivalent or geometry_equivalent,
"brepStable": restored_shape["brepSha256"] == before_shape["brepSha256"],
"restoredGeometrically": True,
}