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, }