988 lines
38 KiB
Python
988 lines
38 KiB
Python
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)
|