Files
Web_FreeCAD_Bitbybit/scripts/freecad-ordered-operation-pair-resave.py
wangdequan 10d11ad608
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: classify pad ordered pair row
2026-08-15 12:51:22 -04:00

313 lines
14 KiB
Python

import hashlib
import json
import os
import FreeCAD as App
import Import
import Part
FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
def version_text():
return ".".join(str(value) for value in App.Version()[:3])
def shape_snapshot_from_shape(shape, include_brep=False):
if shape.isNull():
raise RuntimeError("ordered pair produced a null Shape")
bounds = shape.BoundBox
snapshot = {
"valid": bool(shape.isValid()),
"solids": len(shape.Solids),
"faces": len(shape.Faces),
"edges": len(shape.Edges),
"vertices": len(shape.Vertexes),
"volume": round(float(shape.Volume), 7),
"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):
feature = document.getObject("PairResult")
if feature is None:
raise RuntimeError("PairResult is absent after FCStd persistence")
evidence = str(feature.NamingEvidence)
return {
"pair": str(feature.OrderedPair),
"namingEvidenceBytes": len(evidence.encode("utf-8")),
"namingEvidenceSha256": hashlib.sha256(evidence.encode("utf-8")).hexdigest(),
"shape": shape_snapshot(feature),
}
def document_snapshot(document):
datum_types = {"App::Line", "App::Plane", "App::Point"}
return {
"objects": [{
"name": obj.Name,
"typeId": obj.TypeId,
**({"shape": shape_snapshot(obj)} if obj.TypeId not in datum_types and 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"]
output_directory = os.environ["FREECAD_PAIR_OUTPUT_DIRECTORY"]
with open(evidence_path, "r", encoding="utf-8") as evidence_file:
evidence = evidence_file.read()
document = App.newDocument("FreeCadOrderedPairResave")
try:
Import.insert(step_path, document.Name)
document.recompute()
source_shapes = [obj.Shape for obj in document.Objects if hasattr(obj, "Shape") and not obj.Shape.isNull()]
if not source_shapes:
raise RuntimeError("STEP import produced no Shape")
result = document.addObject("Part::Feature", "PairResult")
result.Shape = source_shapes[0].copy() if len(source_shapes) == 1 else Part.makeCompound([shape.copy() for shape in source_shapes])
result.addProperty("App::PropertyString", "OrderedPair", "Parity")
result.addProperty("App::PropertyString", "NamingEvidence", "Parity")
result.OrderedPair = pair
result.NamingEvidence = evidence
document.recompute()
initial = phase_snapshot(document)
initial_path = os.path.join(output_directory, "ordered-pair.FCStd")
resaved_path = os.path.join(output_directory, "ordered-pair-resaved.FCStd")
document.saveAs(initial_path)
App.closeDocument(document.Name)
reopened = App.openDocument(initial_path)
reopened.recompute()
reopened_snapshot = phase_snapshot(reopened)
reopened.saveAs(resaved_path)
App.closeDocument(reopened.Name)
resaved = App.openDocument(resaved_path)
resaved.recompute()
resaved_snapshot = phase_snapshot(resaved)
App.closeDocument(resaved.Name)
return {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1-ordered-operation-pair-resave",
"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"],
"namingEvidenceStable": initial["namingEvidenceSha256"] == reopened_snapshot["namingEvidenceSha256"] and initial["namingEvidenceSha256"] == resaved_snapshot["namingEvidenceSha256"],
},
"status": "pass",
}
finally:
for name in list(App.listDocuments().keys()):
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()]
solid_sources = [obj for obj in source_objects if obj.TypeId == "Part::Feature" and obj.Shape.isValid() and len(obj.Shape.Solids) == 1]
if len(solid_sources) != 1:
candidates = [{"name": obj.Name, "typeId": obj.TypeId, "shape": shape_snapshot_from_shape(obj.Shape)} for obj in solid_sources]
raise RuntimeError("Rejected-pair STEP import must produce exactly one valid single-Solid source: " + json.dumps(candidates, sort_keys=True))
source = solid_sources[0]
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),
}
elif operation == "revolution":
feature = document.addObject("Part::Revolution", "PairResult")
feature.Source = source
feature.Axis = App.Vector(0.0, 1.0, 0.0)
feature.Base = App.Vector(-1.0, 0.0, 0.0)
feature.Angle = 360.0
feature.Solid = True
diagnostic["parameter"] = {"name": "angle", "value": round(float(feature.Angle), 7)}
diagnostic["input"] = link_snapshot(feature.Source)
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", "revolution": "Part::Revolution"}[operation]
input_names = [diagnostic.get("input", {}).get("name")] if operation in ("pad", "revolution") 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_rejected() if os.environ.get("FREECAD_PAIR_DECISION") == "rejected" else collect_accepted()
except Exception as error:
report = {
"schemaVersion": 1,
"baselineId": "freecad-1.1.1-ordered-operation-pair-resave",
"freecadVersion": version_text(),
"gitCommit": FREECAD_COMMIT,
"status": "failed",
"errorType": type(error).__name__,
"error": str(error),
}
print("FREECAD_ORDERED_OPERATION_PAIR_RESAVE_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))