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)