import json import os import sys import FreeCAD as App import FreeCADGui as Gui import Part import Sketcher from PySide import QtCore, QtWidgets MARKER = "FREECAD_GUI_WORKFLOW_RESULT=" REQUESTED_STATE = os.environ.get("FREECAD_GUI_WORKFLOW_STATE", "success") 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 def active_transaction_state(): transaction = App.getActiveTransaction() return { "active": transaction is not None, "name": str(transaction[0]) if transaction else "", "id": int(transaction[1]) if transaction else None, "documents": [ { "name": str(document.Name), "hasPendingTransaction": bool(document.HasPendingTransaction), "undoCount": counter(document, "UndoCount"), "redoCount": counter(document, "RedoCount"), "undoNames": available_transactions(document, "getAvailableUndoNames"), "redoNames": available_transactions(document, "getAvailableRedoNames"), } for document in App.listDocuments().values() ], } def disabled_side_effect_state(phase): app_document = App.ActiveDocument gui_document = Gui.activeDocument() return { "phase": phase, "documents": sorted(str(name) for name in App.listDocuments()), "documentCount": len(App.listDocuments()), "activeDocument": str(app_document.Name) if app_document else "", "activeGuiDocument": gui_document is not None, "selection": selection_names(), "taskPanel": { "activeDialog": bool(Gui.Control.activeDialog()), "inEdit": active_edit_name(), }, "transaction": active_transaction_state(), } def focus_state(): focus = QtWidgets.QApplication.focusWidget() return None if focus is None else { "className": str(focus.metaObject().className()), "objectName": str(focus.objectName()), } def active_body_name(): active_body = Gui.activeView().getActiveObject("pdbody") if Gui.activeDocument() else None return str(getattr(active_body, "Name", "")) if active_body else "" def run_disabled_workflow(): progress("activate-workbench:start") Gui.activateWorkbench("PartDesignWorkbench") flush_gui() progress("activate-workbench:done") Gui.Selection.clearSelection() flush_gui() command = Gui.Command.get("PartDesign_Pad") actions = list(command.getAction()) if command else [] before_side_effects = disabled_side_effect_state("before") 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, "sideEffects": before_side_effects, } action_signal_count = [0] if actions: actions[0].triggered.connect(lambda _checked=False: action_signal_count.__setitem__(0, action_signal_count[0] + 1)) progress("disabled-action-trigger:start") actions[0].trigger() flush_gui() progress("disabled-action-trigger:done") after_action_trigger = disabled_side_effect_state("after-action-trigger") progress("disabled-run-command:start") Gui.runCommand("PartDesign_Pad", 0) flush_gui() progress("disabled-run-command:done") after_run_command = disabled_side_effect_state("after-run-command") after = { "commandActive": bool(command.isActive()) if command else False, "actionEnabled": bool(actions[0].isEnabled()) if actions else None, "sideEffects": after_run_command, } def stable_side_effects(value): return {key: item for key, item in value.items() if key != "phase"} unchanged_after_action = stable_side_effects(after_action_trigger) == stable_side_effects(before_side_effects) unchanged_after_run_command = stable_side_effects(after_run_command) == stable_side_effects(before_side_effects) 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": "disabled", "before": before, "activationAttempts": { "actionTrigger": { "requested": bool(actions), "triggeredSignalCount": action_signal_count[0], "sideEffectsUnchanged": unchanged_after_action, "after": after_action_trigger, }, "runCommand": { "requested": True, "sideEffectsUnchanged": unchanged_after_run_command, "after": after_run_command, }, }, "after": after, "success": bool( command is not None and actions and not before["commandActive"] and before["actionEnabled"] is False and action_signal_count[0] == 0 and unchanged_after_action and unchanged_after_run_command and after_run_command["documentCount"] == 0 and not after_run_command["taskPanel"]["activeDialog"] and not after_run_command["taskPanel"]["inEdit"] and not after_run_command["transaction"]["active"] and not after_run_command["transaction"]["documents"] ), } progress("result:done") emit(result, 0 if result["success"] else 2) def accept_task_capturing_warning(): captured = [] polls = [0] def capture_warning(): modal = QtWidgets.QApplication.activeModalWidget() if isinstance(modal, QtWidgets.QMessageBox): captured.append({ "title": str(modal.windowTitle()), "text": str(modal.text()), "informativeText": str(modal.informativeText()), "detailedText": str(modal.detailedText()), "standardButtons": int(modal.standardButtons()), }) button = modal.button(QtWidgets.QMessageBox.Ok) if button: button.click() else: modal.accept() return polls[0] += 1 if polls[0] < 200: QtCore.QTimer.singleShot(5, capture_warning) task = Gui.Control.activeTaskDialog() if task is None: return {"requested": False, "warnings": captured, "polls": polls[0]} QtCore.QTimer.singleShot(0, capture_warning) task.accept() flush_gui() return {"requested": True, "warnings": captured, "polls": polls[0]} def run_failure_workflow(): progress("activate-workbench:start") Gui.activateWorkbench("PartDesignWorkbench") flush_gui() progress("activate-workbench:done") document = App.newDocument("PadGuiFailure") 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)] for index in range(2): sketch.addGeometry(Part.LineSegment(points[index], points[index + 1]), False) document.recompute() Gui.activeView().setActiveObject("pdbody", body) Gui.Selection.clearSelection() Gui.Selection.addSelection(document.Name, sketch.Name) flush_gui() baseline = { "objectNames": [str(obj.Name) for obj in document.Objects], "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "", "selection": selection_names(), "sketchGeometryCount": int(sketch.GeometryCount), "sketchShapeValid": bool(sketch.Shape.isValid()), "sketchState": [str(value) for value in sketch.State], "transaction": active_transaction_state(), } 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, "baseline": baseline, } progress("failure-run-command:start") Gui.runCommand("PartDesign_Pad", 0) flush_gui() progress("failure-run-command:done") pad = document.getObject("Pad") opened = task_panel_state("failure-opened") preview = { "exists": pad is not None, "typeId": str(pad.TypeId) if pad else "", "profile": profile_name(pad) if pad else "", "shapeNull": bool(pad.Shape.isNull()) if pad else None, "shapeValid": bool(pad.Shape.isValid()) if pad and not pad.Shape.isNull() else False if pad else None, "state": [str(value) for value in pad.State] if pad else [], "statusString": str(pad.getStatusString()) if pad else "", "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "", "transaction": active_transaction_state(), } progress("failure-accept:start") diagnostic = accept_task_capturing_warning() progress("failure-accept:done") pad = document.getObject("Pad") after_attempt = { "taskPanel": task_panel_state("failure-after-accept"), "objectNames": [str(obj.Name) for obj in document.Objects], "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "", "padExists": pad is not None, "padShapeNull": bool(pad.Shape.isNull()) if pad else None, "padState": [str(value) for value in pad.State] if pad else [], "padStatusString": str(pad.getStatusString()) if pad else "", "transaction": active_transaction_state(), } progress("failure-cleanup:start") task = Gui.Control.activeTaskDialog() cleanup_requested = task is not None if task: task.reject() flush_gui() document.recompute() flush_gui() progress("failure-cleanup:done") after_cleanup = { "rejectRequested": cleanup_requested, "taskPanel": task_panel_state("failure-cleaned"), "objectNames": [str(obj.Name) for obj in document.Objects], "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "", "padExists": document.getObject("Pad") is not None, "selection": selection_names(), "sketchGeometryCount": int(sketch.GeometryCount), "sketchShapeValid": bool(sketch.Shape.isValid()), "sketchState": [str(value) for value in sketch.State], "sketchVisible": bool(sketch.Visibility), "transaction": active_transaction_state(), } warning = diagnostic["warnings"][0] if diagnostic["warnings"] else None 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": "failure", "before": before, "taskPanel": {"opened": opened, "preview": preview, "afterAttempt": after_attempt}, "diagnostic": diagnostic, "after": after_cleanup, "success": bool( before["commandActive"] and opened["activeDialog"] and opened["inEdit"] == "Pad" and preview["exists"] and preview["shapeNull"] and "Invalid" in preview["state"] and diagnostic["requested"] and warning and warning["title"] == "Input error" and warning["text"] == preview["statusString"] and after_attempt["taskPanel"]["activeDialog"] and after_attempt["taskPanel"]["inEdit"] == "Pad" and after_attempt["padExists"] and after_attempt["padShapeNull"] and after_cleanup["rejectRequested"] and not after_cleanup["taskPanel"]["activeDialog"] and not after_cleanup["taskPanel"]["inEdit"] and not after_cleanup["padExists"] and after_cleanup["objectNames"] == baseline["objectNames"] and after_cleanup["bodyTip"] == baseline["bodyTip"] and after_cleanup["sketchGeometryCount"] == baseline["sketchGeometryCount"] and after_cleanup["sketchShapeValid"] == baseline["sketchShapeValid"] and not after_cleanup["transaction"]["active"] ), } progress("result:done") App.closeDocument(document.Name) emit(result, 0 if result["success"] else 2) def run_cancel_workflow(): progress("activate-workbench:start") Gui.activateWorkbench("PartDesignWorkbench") flush_gui() progress("activate-workbench:done") document = App.newDocument("PadGuiCancel") 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) viewport_widgets = [ widget for widget in Gui.getMainWindow().findChildren(QtWidgets.QWidget) if str(widget.metaObject().className()) == "Gui::View3DInventorViewer" ] if viewport_widgets: viewport_widgets[0].setFocus(QtCore.Qt.OtherFocusReason) flush_gui() baseline = { "objectNames": [str(obj.Name) for obj in document.Objects], "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "", "activeBody": active_body_name(), "selection": selection_names(), "focus": focus_state(), "sketchGeometryCount": int(sketch.GeometryCount), "sketchShapeValid": bool(sketch.Shape.isValid()), "transaction": active_transaction_state(), } 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, "baseline": baseline, } progress("cancel-run-command:start") Gui.runCommand("PartDesign_Pad", 0) flush_gui() progress("cancel-run-command:done") opened = task_panel_state("cancel-opened") pad = document.getObject("Pad") if pad: pad.Length = 17.5 document.recompute() flush_gui() preview = { "exists": pad is not None, "typeId": str(pad.TypeId) if pad else "", "length": round(float(pad.Length.Value), 9) if pad else None, "shapeValid": bool(pad.Shape.isValid()) if pad else None, "solidCount": len(pad.Shape.Solids) if pad else None, "volume": round(float(pad.Shape.Volume), 9) if pad else None, "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "", "activeBody": active_body_name(), "selection": selection_names(), "focus": focus_state(), "transaction": active_transaction_state(), } progress("cancel-reject:start") task = Gui.Control.activeTaskDialog() reject_requested = task is not None if task: task.reject() flush_gui() document.recompute() flush_gui() progress("cancel-reject:done") after = { "rejectRequested": reject_requested, "taskPanel": task_panel_state("cancel-closed"), "objectNames": [str(obj.Name) for obj in document.Objects], "bodyTip": str(getattr(body.Tip, "Name", "")) if body.Tip else "", "activeBody": active_body_name(), "padExists": document.getObject("Pad") is not None, "selection": selection_names(), "focus": focus_state(), "sketchGeometryCount": int(sketch.GeometryCount), "sketchShapeValid": bool(sketch.Shape.isValid()), "sketchVisible": bool(sketch.Visibility), "transaction": active_transaction_state(), } 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": "cancel", "before": before, "taskPanel": {"opened": opened, "preview": preview, "closed": after["taskPanel"]}, "after": after, "nativeSelectionOutcome": "selected-profile-consumed", "nativeFocusOutcome": "task-view-after-cancel", "success": bool( before["commandActive"] and baseline["selection"] == ["Sketch"] and baseline["activeBody"] == "Body" and baseline["focus"] == {"className": "Gui::View3DInventorViewer", "objectName": ""} and opened["activeDialog"] and opened["inEdit"] == "Pad" and preview["exists"] and preview["typeId"] == "PartDesign::Pad" and preview["length"] == 17.5 and preview["shapeValid"] and preview["solidCount"] == 1 and abs(preview["volume"] - 210.0) < 1e-7 and preview["selection"] == [] and preview["activeBody"] == baseline["activeBody"] and preview["transaction"]["active"] and after["rejectRequested"] and not after["taskPanel"]["activeDialog"] and not after["taskPanel"]["inEdit"] and not after["padExists"] and after["objectNames"] == baseline["objectNames"] and after["bodyTip"] == baseline["bodyTip"] and after["activeBody"] == baseline["activeBody"] and after["selection"] == [] and after["focus"] == {"className": "Gui::TaskView::TaskView", "objectName": "Tasks"} and after["sketchGeometryCount"] == baseline["sketchGeometryCount"] and after["sketchShapeValid"] == baseline["sketchShapeValid"] and after["sketchVisible"] and not after["transaction"]["active"] ), } progress("result:done") App.closeDocument(document.Name) emit(result, 0 if result["success"] else 2) def recovery_document_state(document, phase): body = document.getObject("Body") sketch = document.getObject("Sketch") pad = document.getObject("Pad") return { "phase": phase, "objectNames": [str(obj.Name) for obj in document.Objects], "bodyGroup": [str(obj.Name) for obj in body.Group] if body else [], "bodyTip": str(getattr(body.Tip, "Name", "")) if body and body.Tip else "", "sketchGeometryCount": int(sketch.GeometryCount) if sketch else None, "sketchShapeValid": bool(sketch.Shape.isValid()) if sketch else None, "padExists": pad is not None, "padTypeId": str(pad.TypeId) if pad else "", "padLength": round(float(pad.Length.Value), 9) if pad else None, "padProfile": profile_name(pad) if pad else "", "padShapeValid": bool(pad.Shape.isValid()) if pad and not pad.Shape.isNull() else False if pad else None, "padSolidCount": len(pad.Shape.Solids) if pad and not pad.Shape.isNull() else 0 if pad else None, "padFaceCount": len(pad.Shape.Faces) if pad and not pad.Shape.isNull() else 0 if pad else None, "padEdgeCount": len(pad.Shape.Edges) if pad and not pad.Shape.isNull() else 0 if pad else None, "padVertexCount": len(pad.Shape.Vertexes) if pad and not pad.Shape.isNull() else 0 if pad else None, "padVolume": round(float(pad.Shape.Volume), 9) if pad and not pad.Shape.isNull() else 0 if pad else None, "padState": [str(value) for value in pad.State] if pad else [], "undoCount": counter(document, "UndoCount"), "redoCount": counter(document, "RedoCount"), "undoNames": available_transactions(document, "getAvailableUndoNames"), "redoNames": available_transactions(document, "getAvailableRedoNames"), "transaction": active_transaction_state(), } def run_recovery_workflow(): recovery_file = os.environ.get("FREECAD_GUI_WORKFLOW_RECOVERY_FILE", "") if not recovery_file: raise RuntimeError("FREECAD_GUI_WORKFLOW_RECOVERY_FILE is required") progress("activate-workbench:start") Gui.activateWorkbench("PartDesignWorkbench") flush_gui() progress("activate-workbench:done") document = App.newDocument("PadGuiRecovery") 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(2): sketch.addGeometry(Part.LineSegment(points[index], points[index + 1]), False) document.recompute() Gui.activeView().setActiveObject("pdbody", body) Gui.Selection.clearSelection() Gui.Selection.addSelection(document.Name, sketch.Name) flush_gui() baseline = recovery_document_state(document, "open-wire-baseline") progress("recovery-run-command:start") Gui.runCommand("PartDesign_Pad", 0) flush_gui() progress("recovery-run-command:done") initial_pad = document.getObject("Pad") initial_failure = { "taskPanel": task_panel_state("recovery-invalid-opened"), "padShapeNull": bool(initial_pad.Shape.isNull()) if initial_pad else None, "padState": [str(value) for value in initial_pad.State] if initial_pad else [], "padStatusString": str(initial_pad.getStatusString()) if initial_pad else "", "diagnostic": accept_task_capturing_warning(), } initial_failure["afterAttempt"] = task_panel_state("recovery-invalid-after-accept") progress("recovery-repair:start") sketch.addGeometry(Part.LineSegment(points[2], points[3]), False) sketch.addGeometry(Part.LineSegment(points[3], points[0]), False) document.recompute() pad = document.getObject("Pad") # FreeCAD exposes Gui::PrefQuantitySpinBox to PySide as a generic QWidget; # rawValue is its double Qt property, while value expects Base::Quantity. length_edit = Gui.getMainWindow().findChild(QtWidgets.QWidget, "lengthEdit") if length_edit: length_edit.setProperty("rawValue", 12.0) flush_gui() elif pad: pad.Length = 12.0 document.recompute() flush_gui() repaired_preview = recovery_document_state(document, "repaired-preview") repaired_preview["lengthInputUpdated"] = bool( length_edit is not None and abs(float(length_edit.property("rawValue")) - 12.0) < 1e-9 ) repaired_preview["taskPanel"] = task_panel_state("recovery-repaired") progress("recovery-repair:done") progress("recovery-accept:start") task = Gui.Control.activeTaskDialog() accept_requested = task is not None if task: task.accept() flush_gui() document.recompute() flush_gui() progress("recovery-accept:done") committed = recovery_document_state(document, "committed") committed["acceptRequested"] = accept_requested committed["taskPanel"] = task_panel_state("recovery-committed") progress("recovery-undo:start") document.undo() document.recompute() flush_gui() undo = recovery_document_state(document, "undo") progress("recovery-undo:done") progress("recovery-redo:start") document.redo() document.recompute() flush_gui() redo = recovery_document_state(document, "redo") progress("recovery-redo:done") progress("recovery-save:start") document.saveAs(recovery_file) flush_gui() saved = recovery_document_state(document, "saved") saved["fileName"] = str(document.FileName) document_name = document.Name App.closeDocument(document_name) reopened_document = App.openDocument(recovery_file) reopened_document.recompute() flush_gui() reopened = recovery_document_state(reopened_document, "reopened") reopened["fileName"] = str(reopened_document.FileName) reopened_document.save() App.closeDocument(reopened_document.Name) resaved_document = App.openDocument(recovery_file) resaved_document.recompute() flush_gui() resaved = recovery_document_state(resaved_document, "resaved-reopened") resaved["fileName"] = str(resaved_document.FileName) progress("recovery-save:done") warning = initial_failure["diagnostic"]["warnings"][0] if initial_failure["diagnostic"]["warnings"] else None def valid_pad_state(value): return bool( value["padExists"] and value["bodyTip"] == "Pad" and value["bodyGroup"] == ["Sketch", "Pad"] and value["sketchGeometryCount"] == 4 and value["padTypeId"] == "PartDesign::Pad" and value["padLength"] == 12 and value["padProfile"] == "Sketch" and value["padShapeValid"] and value["padSolidCount"] == 1 and value["padFaceCount"] == 6 and value["padEdgeCount"] == 12 and value["padVertexCount"] == 8 and abs(value["padVolume"] - 144.0) < 1e-7 and value["padState"] == ["Up-to-date"] and not value["transaction"]["active"] ) 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": "recovery", "before": baseline, "initialFailure": initial_failure, "repairedPreview": repaired_preview, "committed": committed, "undo": undo, "redo": redo, "persistence": {"saved": saved, "reopened": reopened, "resavedReopened": resaved}, "success": bool( baseline["sketchGeometryCount"] == 2 and not baseline["padExists"] and initial_failure["taskPanel"]["activeDialog"] and initial_failure["taskPanel"]["inEdit"] == "Pad" and initial_failure["padShapeNull"] and "Invalid" in initial_failure["padState"] and warning and warning["title"] == "Input error" and warning["text"] == "Wire is not closed." and initial_failure["afterAttempt"]["activeDialog"] and initial_failure["afterAttempt"]["inEdit"] == "Pad" and repaired_preview["taskPanel"]["activeDialog"] and repaired_preview["taskPanel"]["inEdit"] == "Pad" and repaired_preview["lengthInputUpdated"] and repaired_preview["padShapeValid"] and repaired_preview["padVolume"] == 144 and committed["acceptRequested"] and not committed["taskPanel"]["activeDialog"] and not committed["taskPanel"]["inEdit"] and valid_pad_state(committed) and not undo["padExists"] and undo["sketchGeometryCount"] == 2 and undo["bodyTip"] == "" and undo["redoCount"] >= 1 and valid_pad_state(redo) and redo["undoCount"] >= 1 and valid_pad_state(saved) and saved["fileName"] == recovery_file and valid_pad_state(reopened) and reopened["fileName"] == recovery_file and valid_pad_state(resaved) and resaved["fileName"] == recovery_file ), } progress("result:done") App.closeDocument(resaved_document.Name) emit(result, 0 if result["success"] else 2) try: if REQUESTED_STATE == "disabled": run_disabled_workflow() if REQUESTED_STATE == "failure": run_failure_workflow() if REQUESTED_STATE == "cancel": run_cancel_workflow() if REQUESTED_STATE == "recovery": run_recovery_workflow() if REQUESTED_STATE != "success": raise RuntimeError("Unsupported workflow state: " + REQUESTED_STATE) 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": REQUESTED_STATE, "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": REQUESTED_STATE, "success": False, "error": {"type": type(error).__name__, "message": str(error)}, }, 2)