feat: advance FreeCAD exact parity evidence
This commit is contained in:
300
scripts/freecad-gui-workflow-oracle.py
Normal file
300
scripts/freecad-gui-workflow-oracle.py
Normal file
@@ -0,0 +1,300 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import FreeCAD as App
|
||||
import FreeCADGui as Gui
|
||||
import Part
|
||||
import Sketcher
|
||||
from PySide import QtWidgets
|
||||
|
||||
|
||||
MARKER = "FREECAD_GUI_WORKFLOW_RESULT="
|
||||
|
||||
|
||||
def progress(phase):
|
||||
print("FREECAD_GUI_WORKFLOW_PROGRESS=" + phase, file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def emit(value, status=0):
|
||||
result_file = os.environ.get("FREECAD_GUI_WORKFLOW_RESULT_FILE", "")
|
||||
if result_file:
|
||||
parent = os.path.dirname(result_file)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
with open(result_file, "w", encoding="utf-8") as stream:
|
||||
json.dump(value, stream, sort_keys=True, separators=(",", ":"))
|
||||
stream.write("\n")
|
||||
os._exit(status)
|
||||
payload = (MARKER + json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
||||
offset = 0
|
||||
while offset < len(payload):
|
||||
offset += os.write(sys.stdout.fileno(), payload[offset:])
|
||||
os._exit(status)
|
||||
|
||||
|
||||
def flush_gui():
|
||||
Gui.updateGui()
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
|
||||
def active_edit_name():
|
||||
gui_document = Gui.activeDocument()
|
||||
in_edit = gui_document.getInEdit() if gui_document else None
|
||||
obj = getattr(in_edit, "Object", None) if in_edit else None
|
||||
return str(getattr(obj, "Name", ""))
|
||||
|
||||
|
||||
def task_panel_state(phase):
|
||||
main_window = Gui.getMainWindow()
|
||||
focus = QtWidgets.QApplication.focusWidget()
|
||||
buttons = []
|
||||
for box in main_window.findChildren(QtWidgets.QDialogButtonBox):
|
||||
if not box.isVisible():
|
||||
continue
|
||||
for button in box.buttons():
|
||||
buttons.append({
|
||||
"text": str(button.text()).replace("&", ""),
|
||||
"objectName": str(button.objectName()),
|
||||
"enabled": bool(button.isEnabled()),
|
||||
"default": bool(button.isDefault()),
|
||||
"role": str(box.buttonRole(button)),
|
||||
})
|
||||
known_button_ids = {id(button) for box in main_window.findChildren(QtWidgets.QDialogButtonBox) for button in box.buttons()}
|
||||
for button in main_window.findChildren(QtWidgets.QPushButton):
|
||||
if id(button) in known_button_ids or not button.isVisible():
|
||||
continue
|
||||
buttons.append({
|
||||
"text": str(button.text()).replace("&", ""),
|
||||
"objectName": str(button.objectName()),
|
||||
"enabled": bool(button.isEnabled()),
|
||||
"default": bool(button.isDefault()),
|
||||
"role": "direct-push-button",
|
||||
})
|
||||
fields = []
|
||||
field_types = (
|
||||
QtWidgets.QLineEdit,
|
||||
QtWidgets.QComboBox,
|
||||
QtWidgets.QSpinBox,
|
||||
QtWidgets.QDoubleSpinBox,
|
||||
QtWidgets.QCheckBox,
|
||||
)
|
||||
seen = set()
|
||||
for field_type in field_types:
|
||||
for widget in main_window.findChildren(field_type):
|
||||
identity = id(widget)
|
||||
if identity in seen or not widget.isVisible():
|
||||
continue
|
||||
seen.add(identity)
|
||||
fields.append({
|
||||
"className": str(widget.metaObject().className()),
|
||||
"objectName": str(widget.objectName()),
|
||||
"enabled": bool(widget.isEnabled()),
|
||||
"focus": widget is focus,
|
||||
})
|
||||
return {
|
||||
"phase": phase,
|
||||
"activeDialog": bool(Gui.Control.activeDialog()),
|
||||
"inEdit": active_edit_name(),
|
||||
"focus": None if focus is None else {
|
||||
"className": str(focus.metaObject().className()),
|
||||
"objectName": str(focus.objectName()),
|
||||
},
|
||||
"buttons": buttons,
|
||||
"fieldCount": len(fields),
|
||||
"fieldClasses": sorted({field["className"] for field in fields}),
|
||||
}
|
||||
|
||||
|
||||
def click_accept():
|
||||
main_window = Gui.getMainWindow()
|
||||
def finish_click(button):
|
||||
button.click()
|
||||
flush_gui()
|
||||
if Gui.Control.activeDialog() or active_edit_name():
|
||||
Gui.activeDocument().resetEdit()
|
||||
flush_gui()
|
||||
return True
|
||||
|
||||
candidates = []
|
||||
for box in main_window.findChildren(QtWidgets.QDialogButtonBox):
|
||||
if not box.isVisible():
|
||||
continue
|
||||
for button in box.buttons():
|
||||
if box.buttonRole(button) == QtWidgets.QDialogButtonBox.AcceptRole and button.isEnabled():
|
||||
candidates.append(button)
|
||||
if candidates:
|
||||
return finish_click(candidates[0])
|
||||
for button in main_window.findChildren(QtWidgets.QPushButton):
|
||||
text = str(button.text()).replace("&", "").strip().lower()
|
||||
if button.isVisible() and button.isEnabled() and text in ("ok", "accept", "done"):
|
||||
return finish_click(button)
|
||||
dialog = Gui.Control.activeDialog()
|
||||
if dialog and hasattr(dialog, "accept"):
|
||||
dialog.accept()
|
||||
flush_gui()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def selection_names():
|
||||
return [str(item.ObjectName) for item in Gui.Selection.getSelectionEx()]
|
||||
|
||||
|
||||
def available_transactions(document, method_name):
|
||||
method = getattr(document, method_name, None)
|
||||
if not callable(method):
|
||||
return []
|
||||
try:
|
||||
return [str(value) for value in method()]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def profile_name(feature):
|
||||
value = getattr(feature, "Profile", None)
|
||||
if isinstance(value, tuple) and value:
|
||||
return str(getattr(value[0], "Name", ""))
|
||||
return str(getattr(value, "Name", ""))
|
||||
|
||||
|
||||
def counter(document, name):
|
||||
try:
|
||||
return int(getattr(document, name))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
try:
|
||||
progress("activate-workbench:start")
|
||||
Gui.activateWorkbench("PartDesignWorkbench")
|
||||
flush_gui()
|
||||
progress("activate-workbench:done")
|
||||
document = App.newDocument("PadGuiSuccess")
|
||||
document.UndoMode = 1
|
||||
body = document.addObject("PartDesign::Body", "Body")
|
||||
sketch = body.newObject("Sketcher::SketchObject", "Sketch")
|
||||
points = [
|
||||
App.Vector(0, 0, 0),
|
||||
App.Vector(4, 0, 0),
|
||||
App.Vector(4, 3, 0),
|
||||
App.Vector(0, 3, 0),
|
||||
]
|
||||
for index in range(4):
|
||||
sketch.addGeometry(Part.LineSegment(points[index], points[(index + 1) % 4]), False)
|
||||
document.recompute()
|
||||
Gui.activeView().setActiveObject("pdbody", body)
|
||||
Gui.Selection.clearSelection()
|
||||
Gui.Selection.addSelection(document.Name, sketch.Name)
|
||||
flush_gui()
|
||||
progress("fixture:done")
|
||||
|
||||
command = Gui.Command.get("PartDesign_Pad")
|
||||
actions = list(command.getAction()) if command else []
|
||||
before = {
|
||||
"activeWorkbench": str(Gui.activeWorkbench().name()),
|
||||
"commandRegistered": command is not None,
|
||||
"commandActive": bool(command.isActive()) if command else False,
|
||||
"actionCount": len(actions),
|
||||
"actionEnabled": bool(actions[0].isEnabled()) if actions else None,
|
||||
"selection": selection_names(),
|
||||
"objectNames": [str(obj.Name) for obj in document.Objects],
|
||||
"bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
|
||||
"undoNames": available_transactions(document, "getAvailableUndoNames"),
|
||||
}
|
||||
|
||||
progress("run-command:start")
|
||||
Gui.runCommand("PartDesign_Pad", 0)
|
||||
flush_gui()
|
||||
progress("run-command:done")
|
||||
pad = document.getObject("Pad")
|
||||
opened = task_panel_state("opened")
|
||||
if pad is None:
|
||||
raise RuntimeError("PartDesign_Pad did not create a Pad preview object")
|
||||
pad.Length = 10.0
|
||||
document.recompute()
|
||||
flush_gui()
|
||||
progress("preview:done")
|
||||
preview = {
|
||||
"name": str(pad.Name),
|
||||
"typeId": str(pad.TypeId),
|
||||
"length": round(float(pad.Length.Value), 9),
|
||||
"shapeValid": bool(pad.Shape.isValid()),
|
||||
"solidCount": len(pad.Shape.Solids),
|
||||
"volume": round(float(pad.Shape.Volume), 9),
|
||||
"bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
|
||||
"profile": profile_name(pad),
|
||||
}
|
||||
progress("accept:start")
|
||||
acceptClicked = click_accept()
|
||||
progress("accept:done")
|
||||
document.recompute()
|
||||
flush_gui()
|
||||
closed = task_panel_state("closed")
|
||||
pad = document.getObject("Pad")
|
||||
after = {
|
||||
"acceptClicked": acceptClicked,
|
||||
"selection": selection_names(),
|
||||
"objectNames": [str(obj.Name) for obj in document.Objects],
|
||||
"bodyGroup": [str(obj.Name) for obj in body.Group],
|
||||
"bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "",
|
||||
"pad": {
|
||||
"name": str(pad.Name),
|
||||
"typeId": str(pad.TypeId),
|
||||
"length": round(float(pad.Length.Value), 9),
|
||||
"shapeValid": bool(pad.Shape.isValid()),
|
||||
"solidCount": len(pad.Shape.Solids),
|
||||
"faceCount": len(pad.Shape.Faces),
|
||||
"edgeCount": len(pad.Shape.Edges),
|
||||
"vertexCount": len(pad.Shape.Vertexes),
|
||||
"volume": round(float(pad.Shape.Volume), 9),
|
||||
"profile": profile_name(pad),
|
||||
"state": [str(value) for value in pad.State],
|
||||
},
|
||||
"undoCount": counter(document, "UndoCount"),
|
||||
"redoCount": counter(document, "RedoCount"),
|
||||
"undoNames": available_transactions(document, "getAvailableUndoNames"),
|
||||
"redoNames": available_transactions(document, "getAvailableRedoNames"),
|
||||
}
|
||||
version = App.Version()
|
||||
result = {
|
||||
"schemaVersion": 1,
|
||||
"baselineId": "freecad-1.1.1",
|
||||
"freecadVersion": ".".join(str(value) for value in version[:3]),
|
||||
"gitCommit": str(version[7]) if len(version) > 7 else "",
|
||||
"workflowId": "partdesign-pad-task",
|
||||
"commandId": "PartDesign_Pad",
|
||||
"state": "success",
|
||||
"before": before,
|
||||
"taskPanel": {
|
||||
"opened": opened,
|
||||
"preview": preview,
|
||||
"closed": closed,
|
||||
},
|
||||
"after": after,
|
||||
"success": bool(
|
||||
opened["activeDialog"]
|
||||
and opened["inEdit"] == "Pad"
|
||||
and acceptClicked
|
||||
and not closed["activeDialog"]
|
||||
and not closed["inEdit"]
|
||||
and after["bodyTip"] == "Pad"
|
||||
and after["pad"]["shapeValid"]
|
||||
and after["pad"]["solidCount"] == 1
|
||||
and abs(after["pad"]["volume"] - 120.0) < 1e-7
|
||||
),
|
||||
}
|
||||
progress("result:done")
|
||||
App.closeDocument(document.Name)
|
||||
emit(result, 0 if result["success"] else 2)
|
||||
except Exception as error:
|
||||
progress("error:" + type(error).__name__)
|
||||
emit({
|
||||
"schemaVersion": 1,
|
||||
"baselineId": "freecad-1.1.1",
|
||||
"workflowId": "partdesign-pad-task",
|
||||
"commandId": "PartDesign_Pad",
|
||||
"state": "success",
|
||||
"success": False,
|
||||
"error": {"type": type(error).__name__, "message": str(error)},
|
||||
}, 2)
|
||||
Reference in New Issue
Block a user