572 lines
30 KiB
Python
572 lines
30 KiB
Python
"""Native camera and Qt sketch-workspace regressions.
|
|
|
|
The drawing case starts with an empty native sketch. Rectangle geometry, edge
|
|
selection, both driving dimensions and dimension-list editing use actual Qt
|
|
events. Camera fixtures use the model API; they do not substitute for drawing.
|
|
"""
|
|
import json
|
|
import hashlib
|
|
import math
|
|
import os
|
|
import traceback
|
|
|
|
import FreeCAD as App
|
|
import FreeCADGui as Gui
|
|
import Part
|
|
from PySide import QtCore, QtGui
|
|
try:
|
|
from PySide import QtWidgets
|
|
except ImportError:
|
|
QtWidgets = QtGui
|
|
try:
|
|
from PySide import QtTest
|
|
except ImportError:
|
|
try:
|
|
from PySide6 import QtTest, QtOpenGLWidgets
|
|
except ImportError:
|
|
from PySide2 import QtTest
|
|
|
|
|
|
def run(output):
|
|
import qi_commands as commands
|
|
import qi_model as model
|
|
import qi_ui as ui
|
|
|
|
os.makedirs(output, exist_ok=True)
|
|
path = os.path.join(output, "sketch-workspace-suite.json")
|
|
report = {"status": "running", "passed": 0, "failed": 0, "checks": [],
|
|
"input_delivery": "Qt mouse/key events on native CAD widgets",
|
|
"qt_cursor_positioning": True, "physical_desktop_input": False}
|
|
created = []
|
|
initial_active = App.ActiveDocument.Name if App.ActiveDocument else None
|
|
main = Gui.getMainWindow()
|
|
previous_size = main.size()
|
|
preferences = []
|
|
context = {}
|
|
|
|
def settle(ms=120):
|
|
QtWidgets.QApplication.processEvents()
|
|
QtTest.QTest.qWait(ms)
|
|
|
|
def wait_for(predicate, message, timeout=1800):
|
|
elapsed = QtCore.QElapsedTimer()
|
|
elapsed.start()
|
|
while not predicate() and elapsed.elapsed() < timeout:
|
|
settle(40)
|
|
assert predicate(), {"message": message, "waited_ms": elapsed.elapsed(),
|
|
"editing": getattr(commands._editing(), "Name", None),
|
|
"dimension_shortcut_enabled": ribbon.shortcuts["QI_Dimension"].isEnabled(),
|
|
"native_dimension_active": Gui.Command.get("Sketcher_Dimension").isActive()}
|
|
return elapsed.elapsed()
|
|
|
|
def write():
|
|
with open(path, "w", encoding="utf-8") as stream:
|
|
json.dump(report, stream, ensure_ascii=False, indent=2, default=str)
|
|
|
|
def capture(name):
|
|
destination = os.path.join(output, name + ".png")
|
|
assert main.grab().save(destination), destination
|
|
return destination
|
|
|
|
def check(name, function):
|
|
try:
|
|
evidence = function()
|
|
report["checks"].append({"name": name, "passed": True, "evidence": evidence})
|
|
report["passed"] += 1
|
|
except Exception:
|
|
failure = {"name": name, "passed": False, "error": traceback.format_exc()}
|
|
if "hole_pick_diagnostics" in context:
|
|
failure["hole_pick_diagnostics"] = context["hole_pick_diagnostics"]
|
|
try:
|
|
failure["screenshot"] = capture("sketch-workspace-failure-%d" % (report["failed"] + 1))
|
|
except Exception:
|
|
failure["capture_error"] = traceback.format_exc()
|
|
report["checks"].append(failure)
|
|
report["failed"] += 1
|
|
write()
|
|
|
|
def set_preference(group_path, key, value):
|
|
group = App.ParamGet(group_path)
|
|
present = any(entry[1] == key for entry in (group.GetContents() or []))
|
|
preferences.append((group, key, present, group.GetBool(key)))
|
|
group.SetBool(key, value)
|
|
|
|
def own_document(title):
|
|
document = model.new_part(title)
|
|
created.append(document.Name)
|
|
return document
|
|
|
|
def close_owned(document):
|
|
if document.Name in App.listDocuments():
|
|
if App.ActiveDocument == document and commands._editing():
|
|
Gui.activeDocument().resetEdit()
|
|
settle()
|
|
App.closeDocument(document.Name)
|
|
|
|
def renderer():
|
|
candidates = [widget for widget in main.findChildren(QtWidgets.QWidget)
|
|
if widget.isVisible() and widget.metaObject().className() == "QOpenGLWidget"
|
|
and widget.parent() and widget.parent().metaObject().className() == "Gui::View3DInventorViewer"]
|
|
assert candidates, "Native OpenGL drafting viewport is missing"
|
|
widget = max(candidates, key=lambda item: item.width() * item.height())
|
|
if "QtOpenGLWidgets" in globals():
|
|
import shiboken6
|
|
widget = shiboken6.wrapInstance(shiboken6.getCppPointer(widget)[0], QtOpenGLWidgets.QOpenGLWidget)
|
|
return widget
|
|
|
|
def camera_frame(view):
|
|
node = view.getCameraNode()
|
|
return {"position": list(node.position.getValue().getValue()),
|
|
"orientation": list(node.orientation.getValue().getValue()),
|
|
"height": float(node.height.getValue())}
|
|
|
|
def assert_camera(actual, expected, message):
|
|
assert abs(actual["height"] - expected["height"]) <= 1e-4, (message, actual, expected)
|
|
assert max(abs(a - b) for a, b in zip(actual["position"], expected["position"])) <= 1e-4, (message, actual, expected)
|
|
a, b = actual["orientation"], expected["orientation"]
|
|
cosine = abs(sum(x * y for x, y in zip(a, b))) / math.sqrt(sum(x*x for x in a) * sum(y*y for y in b))
|
|
assert abs(1 - cosine) <= 1e-7, (message, actual, expected)
|
|
|
|
def native_camera_roundtrip():
|
|
document = own_document("Камера при завершении и отмене")
|
|
try:
|
|
sketch = model.create_sketch_profile("rectangle", 40, 25, doc=document)
|
|
view = Gui.activeDocument().activeView()
|
|
view.setCameraType("Orthographic")
|
|
result = []
|
|
for command in ("Sketcher_LeaveSketch", "QI_FinishSketch", "QI_CancelSketch"):
|
|
view.setCameraOrientation(App.Rotation(App.Vector(1, 2, 3), 31).Q)
|
|
node = view.getCameraNode()
|
|
node.position.setValue(75.0, 60.0, 320.0)
|
|
node.height.setValue(233.0)
|
|
settle(400)
|
|
original = camera_frame(view)
|
|
original_count = sketch.GeometryCount
|
|
Gui.activeDocument().setEdit(sketch.Name)
|
|
settle(900)
|
|
if command == "QI_CancelSketch":
|
|
sketch.addGeometry(Part.LineSegment(App.Vector(70, 0, 0), App.Vector(70, 20, 0)), True)
|
|
document.recompute()
|
|
Gui.runCommand(command, 0)
|
|
settle(400)
|
|
first = camera_frame(view)
|
|
settle(1100)
|
|
second = camera_frame(view)
|
|
assert commands._editing() is None
|
|
assert_camera(first, original, command + " must preserve native RestoreCamera")
|
|
assert_camera(second, first, command + " changed camera after its initial layout settled")
|
|
assert sketch.GeometryCount == original_count
|
|
result.append({"command": command, "original": original,
|
|
"after_400ms": first, "after_1500ms": second})
|
|
result.append({"screenshot": capture("sketch-camera-restored")})
|
|
return result
|
|
finally:
|
|
close_owned(document)
|
|
|
|
def empty_sketch():
|
|
document = own_document("Построение эскиза мышью")
|
|
chooser = ui.choose_plane
|
|
ui.choose_plane = lambda: "XY"
|
|
try:
|
|
Gui.runCommand("QI_NewSketch", 0)
|
|
finally:
|
|
ui.choose_plane = chooser
|
|
settle(1000)
|
|
sketch = commands._editing()
|
|
assert sketch and sketch.Document == document and sketch.GeometryCount == 0
|
|
assert main.findChild(QtWidgets.QTableWidget, "QI_SketchDimensionList") is not None
|
|
context.update(document=document, sketch=sketch)
|
|
view = Gui.activeDocument().activeView()
|
|
first = camera_frame(view)
|
|
assert 80 <= first["height"] <= 240, ("Initial sketch scale is unsuitable for drafting millimetre-sized parts", first)
|
|
settle(1100)
|
|
second = camera_frame(view)
|
|
assert_camera(second, first, "Empty sketch camera changed while idle")
|
|
return {"geometry_count": 0, "plane_selection": "XY test fixture chooser",
|
|
"camera_after_layout": first, "camera_after_idle": second}
|
|
|
|
def pinned_controls():
|
|
assert commands._editing() == context["sketch"]
|
|
results = []
|
|
for width, height in ((1366, 768), (1440, 940)):
|
|
main.resize(width, height)
|
|
settle(500)
|
|
assert main.width() == width and main.height() == height, ("Window cannot reach requested size", main.size())
|
|
states = []
|
|
for title in ("Эскиз", "Вид", "3D-модель", "Эскиз"):
|
|
index = next(i for i in range(ribbon.tabs.count()) if ribbon.tabs.tabText(i) == title)
|
|
QtTest.QTest.mouseClick(ribbon.tabs.tabBar(), QtCore.Qt.LeftButton, QtCore.Qt.NoModifier,
|
|
ribbon.tabs.tabBar().tabRect(index).center())
|
|
settle(100)
|
|
widget = renderer()
|
|
rectangle = widget.mapTo(main, QtCore.QPoint())
|
|
states.append([rectangle.x(), rectangle.y(), widget.width(), widget.height()])
|
|
for name in ("QI_FinishSketch", "QI_CancelSketch"):
|
|
buttons = ribbon.buttons.get(name, [])
|
|
assert buttons, "Missing sketch completion command: " + name
|
|
assert any(button.isVisible() and button.isEnabled()
|
|
and button.visibleRegion().contains(button.rect()) for button in buttons), (name, width, title)
|
|
assert ribbon.height() == 174
|
|
assert all(state == states[0] for state in states), ("Changing ribbon tab moved the CAD viewport", states)
|
|
results.append({"window_size": [width, height], "viewport_rectangles": states,
|
|
"screenshot": capture("sketch-workspace-%dx%d" % (width, height))})
|
|
return results
|
|
|
|
def move(widget, point):
|
|
global_point = widget.mapToGlobal(point)
|
|
QtGui.QCursor.setPos(global_point)
|
|
event = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, QtCore.QPointF(point),
|
|
QtCore.QPointF(global_point), QtCore.Qt.NoButton,
|
|
QtCore.Qt.NoButton, QtCore.Qt.NoModifier)
|
|
QtWidgets.QApplication.sendEvent(widget, event)
|
|
|
|
def click_canvas(point, wait=80):
|
|
widget = renderer()
|
|
move(widget, point)
|
|
settle(wait)
|
|
QtTest.QTest.mousePress(widget, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, point)
|
|
settle(60)
|
|
QtTest.QTest.mouseRelease(widget, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, point)
|
|
settle(wait)
|
|
|
|
def mouse_rectangle():
|
|
sketch = context["sketch"]
|
|
assert commands._editing() == sketch and sketch.GeometryCount == 0
|
|
button = ribbon.buttons["QI_Rectangle"][0]
|
|
assert button.isVisible() and button.isEnabled()
|
|
QtTest.QTest.mouseClick(button, QtCore.Qt.LeftButton)
|
|
settle(100)
|
|
widget = renderer()
|
|
click_canvas(QtCore.QPoint(round(widget.width() * .28), round(widget.height() * .64)))
|
|
click_canvas(QtCore.QPoint(round(widget.width() * .54), round(widget.height() * .36)))
|
|
QtTest.QTest.keyClick(widget, QtCore.Qt.Key_Escape)
|
|
settle(250)
|
|
wait_for(lambda: ribbon.shortcuts["QI_Dimension"].isEnabled(),
|
|
"Dimension shortcut did not become available after leaving the rectangle tool")
|
|
assert sketch.GeometryCount == 4
|
|
assert all(geometry.__class__.__name__ == "LineSegment" for geometry in sketch.Geometry)
|
|
assert Part.Wire([geometry.toShape() for geometry in sketch.Geometry]).isClosed()
|
|
assert commands._editing() == sketch
|
|
return {"geometry_count": 4, "closed_native_wire": True,
|
|
"screenshot": capture("sketch-mouse-rectangle")}
|
|
|
|
def project(local_point):
|
|
sketch = context["sketch"]
|
|
widget = renderer()
|
|
view = Gui.activeDocument().activeView()
|
|
projected = view.getPointOnScreen(sketch.getGlobalPlacement().multVec(local_point))
|
|
size = view.getViewer().getSoRenderManager().getViewportRegion().getViewportSizePixels().getValue()
|
|
return QtCore.QPoint(round(float(projected[0]) * widget.width() / size[0]),
|
|
round(widget.height() - float(projected[1]) * widget.height() / size[1]))
|
|
|
|
def arm_datum(text):
|
|
existing = {id(widget) for widget in QtWidgets.QApplication.topLevelWidgets()}
|
|
evidence = {"value": text, "dialogs": [], "timed_out": False}
|
|
timer = QtCore.QTimer(main)
|
|
attempts = [0]
|
|
def enter():
|
|
attempts[0] += 1
|
|
dialogs = [widget for widget in QtWidgets.QApplication.topLevelWidgets()
|
|
if isinstance(widget, QtWidgets.QDialog) and widget.isVisible() and id(widget) not in existing]
|
|
for dialog in dialogs:
|
|
edits = [edit for edit in dialog.findChildren(QtWidgets.QLineEdit) if edit.isVisible() and edit.isEnabled()]
|
|
if edits:
|
|
edit = edits[0]
|
|
edit.setFocus()
|
|
edit.selectAll()
|
|
QtTest.QTest.keyClicks(edit, text)
|
|
QtTest.QTest.keyClick(edit, QtCore.Qt.Key_Return)
|
|
evidence["dialogs"].append(dialog.windowTitle())
|
|
timer.stop()
|
|
return
|
|
if attempts[0] >= 40:
|
|
evidence["timed_out"] = True
|
|
timer.stop()
|
|
for dialog in dialogs:
|
|
dialog.reject()
|
|
timer.timeout.connect(enter)
|
|
timer.start(100)
|
|
return timer, evidence
|
|
|
|
def finish_datum(timer, evidence):
|
|
settle(350)
|
|
timer.stop()
|
|
timer.deleteLater()
|
|
assert evidence["dialogs"] and not evidence["timed_out"], ("Native datum dialog was not exercised", evidence)
|
|
return evidence
|
|
|
|
def set_mouse_dimension(horizontal, value):
|
|
sketch = context["sketch"]
|
|
geometry = [(index, item) for index, item in enumerate(sketch.Geometry)
|
|
if abs((item.StartPoint.y - item.EndPoint.y) if horizontal else (item.StartPoint.x - item.EndPoint.x)) < 1e-6]
|
|
index, edge = max(geometry, key=lambda item: item[1].StartPoint.y if horizontal else item[1].StartPoint.x)
|
|
expected = "Edge%d" % (index + 1)
|
|
Gui.Selection.clearSelection()
|
|
point = project(edge.StartPoint * .35 + edge.EndPoint * .65)
|
|
widget = renderer()
|
|
move(widget, point)
|
|
settle(160)
|
|
preselection = Gui.Selection.getPreselection()
|
|
assert getattr(preselection, "ObjectName", None) == sketch.Name and expected in preselection.SubElementNames
|
|
click_canvas(point)
|
|
assert any(item.Object == sketch and expected in item.SubElementNames for item in Gui.Selection.getSelectionEx()), expected
|
|
widget.setFocus(QtCore.Qt.OtherFocusReason)
|
|
main.activateWindow()
|
|
QtWidgets.QApplication.setActiveWindow(main)
|
|
settle(150)
|
|
target = QtWidgets.QApplication.focusWidget()
|
|
assert target and (target == widget or target.isAncestorOf(widget) or widget.isAncestorOf(target))
|
|
shortcut_wait_ms = wait_for(lambda: ribbon.shortcuts["QI_Dimension"].isEnabled(),
|
|
"Dimension shortcut did not reflect the native selection state")
|
|
timer, evidence = arm_datum("%s mm" % value)
|
|
try:
|
|
QtTest.QTest.keyClick(target, QtCore.Qt.Key_D)
|
|
settle(100)
|
|
placement = point + (QtCore.QPoint(0, -50) if horizontal else QtCore.QPoint(60, 0))
|
|
assert widget.rect().contains(placement), ("Dimension placement leaves viewport", placement, widget.rect())
|
|
click_canvas(placement)
|
|
finish_datum(timer, evidence)
|
|
finally:
|
|
timer.stop()
|
|
QtTest.QTest.keyClick(widget, QtCore.Qt.Key_Escape)
|
|
settle(450)
|
|
wait_for(lambda: ribbon.shortcuts["QI_Dimension"].isEnabled(),
|
|
"Dimension shortcut did not become available after leaving the dimension tool")
|
|
matches = [index for index, constraint in enumerate(sketch.Constraints)
|
|
if constraint.Type in ("Distance", "DistanceX", "DistanceY")
|
|
and constraint.Driving and abs(abs(constraint.Value) - value) < 1e-5]
|
|
assert matches and sketch.solve() == 0, [(item.Type, item.Value) for item in sketch.Constraints]
|
|
return {"edge": expected, "value_mm": value, "constraint_indices": matches,
|
|
"input_selection": "native viewport Qt mouse events", "native_editor": evidence,
|
|
"shortcut_wait_ms": shortcut_wait_ms}
|
|
|
|
def dimensions_and_list_edit():
|
|
sketch = context["sketch"]
|
|
assert sketch.GeometryCount == 4
|
|
dimensions = [set_mouse_dimension(True, 40), set_mouse_dimension(False, 25)]
|
|
bounds = Part.Wire([geometry.toShape() for geometry in sketch.Geometry]).BoundBox
|
|
assert abs(bounds.XLength - 40) < 1e-5 and abs(bounds.YLength - 25) < 1e-5, bounds
|
|
settle(800)
|
|
table = main.findChild(QtWidgets.QTableWidget, "QI_SketchDimensionList")
|
|
assert table is not None and table.isVisible() and table.rowCount() == 2
|
|
values = []
|
|
row40 = None
|
|
for row in range(table.rowCount()):
|
|
index = int(table.item(row, 0).data(QtCore.Qt.UserRole))
|
|
value = abs(float(sketch.getDatum(index).Value))
|
|
values.append({"constraint_index": index, "value_mm": value, "display": table.item(row, 1).text()})
|
|
if abs(value - 40) < 1e-5:
|
|
row40 = row
|
|
assert row40 is not None and any(abs(item["value_mm"] - 25) < 1e-5 for item in values)
|
|
item = table.item(row40, 0)
|
|
table.scrollToItem(item)
|
|
QtTest.QTest.mouseClick(table.viewport(), QtCore.Qt.LeftButton, QtCore.Qt.NoModifier,
|
|
table.visualItemRect(item).center())
|
|
settle(150)
|
|
index = int(item.data(QtCore.Qt.UserRole))
|
|
expected = "Constraint%d" % (index + 1)
|
|
assert any(selection.Object == sketch and expected in selection.SubElementNames
|
|
for selection in Gui.Selection.getSelectionEx()), "Dimension list did not select native constraint"
|
|
timer, edit = arm_datum("60 mm")
|
|
try:
|
|
QtTest.QTest.mouseDClick(table.viewport(), QtCore.Qt.LeftButton, QtCore.Qt.NoModifier,
|
|
table.visualItemRect(item).center())
|
|
finish_datum(timer, edit)
|
|
finally:
|
|
timer.stop()
|
|
settle(800)
|
|
assert abs(abs(float(sketch.getDatum(index).Value)) - 60) < 1e-5
|
|
bounds = Part.Wire([geometry.toShape() for geometry in sketch.Geometry]).BoundBox
|
|
assert abs(bounds.XLength - 60) < 1e-5 and abs(bounds.YLength - 25) < 1e-5 and sketch.solve() == 0
|
|
evidence = {"native_dimensions": dimensions, "dimension_list": values,
|
|
"native_selected_constraint": expected, "edited_value_mm": 60,
|
|
"native_edit_dialog": edit, "screenshot": capture("sketch-two-dimensions-edited")}
|
|
Gui.runCommand("QI_FinishSketch", 0)
|
|
settle(450)
|
|
assert commands._editing() is None
|
|
return evidence
|
|
|
|
def mouse_hole_and_native_pad():
|
|
document, sketch = context["document"], context["sketch"]
|
|
assert document.Name in App.listDocuments() and sketch.GeometryCount == 4
|
|
bounds = Part.Wire([geometry.toShape() for geometry in sketch.Geometry]).BoundBox
|
|
assert abs(bounds.XLength - 60) < 1e-5 and abs(bounds.YLength - 25) < 1e-5, {
|
|
"message": "The hole workflow requires the previous dimension workflow to finish at 60x25 mm",
|
|
"actual_width": bounds.XLength, "actual_height": bounds.YLength,
|
|
"native_constraints": [(item.Type, item.Value) for item in sketch.Constraints]}
|
|
Gui.activeDocument().setEdit(sketch.Name)
|
|
settle(900)
|
|
circle_button = ribbon.buttons["QI_Circle"][0]
|
|
assert circle_button.isVisible() and circle_button.isEnabled()
|
|
QtTest.QTest.mouseClick(circle_button, QtCore.Qt.LeftButton)
|
|
settle(100)
|
|
click_canvas(project(bounds.Center))
|
|
click_canvas(project(bounds.Center + App.Vector(5, 0, 0)))
|
|
QtTest.QTest.keyClick(renderer(), QtCore.Qt.Key_Escape)
|
|
settle(350)
|
|
assert sketch.GeometryCount == 5, "Circle tool did not add exactly one native geometry"
|
|
circle_index = 4
|
|
circle = sketch.Geometry[circle_index]
|
|
assert circle.__class__.__name__ == "Circle"
|
|
expected_edge = "Edge%d" % (circle_index + 1)
|
|
local_pick = circle.Center + App.Vector(circle.Radius / math.sqrt(2), circle.Radius / math.sqrt(2), 0)
|
|
point = project(local_pick)
|
|
widget = renderer()
|
|
view = Gui.activeDocument().activeView()
|
|
physical = view.getViewer().getSoRenderManager().getViewportRegion().getViewportSizePixels().getValue()
|
|
focus = QtWidgets.QApplication.focusWidget()
|
|
diagnostics = {"circle_center": list(circle.Center), "circle_radius": circle.Radius,
|
|
"local_pick_point": list(local_pick),
|
|
"world_pick_point": list(sketch.getGlobalPlacement().multVec(local_pick)),
|
|
"projected_physical": list(view.getPointOnScreen(sketch.getGlobalPlacement().multVec(local_pick))),
|
|
"physical_viewport_size": list(physical), "qt_pick": [point.x(), point.y()],
|
|
"qt_renderer_size": [widget.width(), widget.height()],
|
|
"qt_renderer_global_origin": [widget.mapToGlobal(QtCore.QPoint()).x(), widget.mapToGlobal(QtCore.QPoint()).y()],
|
|
"qt_dpr": widget.devicePixelRatioF(), "camera": camera_frame(view),
|
|
"editing": getattr(commands._editing(), "Name", None), "expected_edge": expected_edge,
|
|
"active_workbench": Gui.activeWorkbench().name(), "geometry_count": sketch.GeometryCount,
|
|
"focus_widget": focus.metaObject().className() if focus else None,
|
|
"focus_object_name": focus.objectName() if focus else None,
|
|
"tool_hint": ribbon.hint.text(), "main_active": main.isActiveWindow()}
|
|
context["hole_pick_diagnostics"] = diagnostics
|
|
assert widget.rect().contains(point), diagnostics
|
|
Gui.Selection.clearSelection()
|
|
move(widget, point)
|
|
settle(160)
|
|
preselection = Gui.Selection.getPreselection()
|
|
diagnostics["hover_preselection"] = {
|
|
"object_name": getattr(preselection, "ObjectName", None),
|
|
"document_name": getattr(preselection, "DocumentName", None),
|
|
"subelements": list(getattr(preselection, "SubElementNames", []))}
|
|
diagnostics["before_pick_screenshot"] = capture("sketch-hole-before-radius-pick")
|
|
framebuffer = view.getViewer().grabFramebuffer()
|
|
framebuffer_path = os.path.join(output, "sketch-hole-before-radius-pick-native.png")
|
|
assert not framebuffer.isNull() and framebuffer.save(framebuffer_path), diagnostics
|
|
diagnostics["native_framebuffer"] = framebuffer_path
|
|
# Retain the provisional hover evidence, then require the actual native
|
|
# edge selection after the Qt gesture. Never substitute addSelection().
|
|
click_canvas(point)
|
|
selection = Gui.Selection.getSelectionEx()
|
|
diagnostics["selection_after_click"] = [
|
|
{"object_name": item.Object.Name, "subelements": list(item.SubElementNames)} for item in selection]
|
|
with open(os.path.join(output, "sketch-hole-pick-diagnostics.json"), "w", encoding="utf-8") as stream:
|
|
json.dump(diagnostics, stream, ensure_ascii=False, indent=2, default=str)
|
|
assert any(item.Object == sketch and expected_edge in item.SubElementNames for item in selection), diagnostics
|
|
button = ribbon.buttons["QI_Dimension"][0]
|
|
menu = button.menu()
|
|
assert menu is not None
|
|
action = next((item for item in menu.actions()
|
|
if item.text() == commands.COMMANDS["QI_DimensionRadius"].title), None)
|
|
assert action is not None, "Radius command is missing from the dimension menu"
|
|
menu.popup(button.mapToGlobal(QtCore.QPoint(0, button.height())))
|
|
settle(100)
|
|
assert menu.isVisible()
|
|
wait_for(action.isEnabled, "Radius action did not become available in the open dimension menu")
|
|
timer, datum = arm_datum("5 mm")
|
|
try:
|
|
QtTest.QTest.mouseClick(menu, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier,
|
|
menu.actionGeometry(action).center())
|
|
finish_datum(timer, datum)
|
|
finally:
|
|
timer.stop()
|
|
menu.hide()
|
|
QtTest.QTest.keyClick(renderer(), QtCore.Qt.Key_Escape)
|
|
settle(450)
|
|
radius_constraints = [index for index, constraint in enumerate(sketch.Constraints)
|
|
if constraint.Type == "Radius" and constraint.First == circle_index
|
|
and constraint.Driving and abs(constraint.Value - 5) < 1e-5]
|
|
assert radius_constraints and sketch.solve() == 0
|
|
circle = sketch.Geometry[circle_index]
|
|
assert abs(circle.Radius - 5) < 1e-5
|
|
assert bounds.XMin < circle.Center.x - circle.Radius < circle.Center.x + circle.Radius < bounds.XMax
|
|
assert bounds.YMin < circle.Center.y - circle.Radius < circle.Center.y + circle.Radius < bounds.YMax
|
|
settle(450)
|
|
table = main.findChild(QtWidgets.QTableWidget, "QI_SketchDimensionList")
|
|
assert table is not None and table.isVisible() and table.rowCount() == 3
|
|
sketch_evidence = {"circle_input": "Qt Circle button and native viewport mouse events",
|
|
"radius_command_input": "Qt click on Radius action in the dimension menu",
|
|
"native_circle_index": circle_index, "native_center": list(circle.Center),
|
|
"rectangle_center": list(bounds.Center), "center_is_pixel_positioned": True,
|
|
"radius_mm": circle.Radius, "fully_inside_rectangle": True,
|
|
"radius_constraints": radius_constraints, "native_datum_dialog": datum,
|
|
"native_circle_selection": diagnostics,
|
|
"screenshot": capture("sketch-rectangle-with-hole")}
|
|
Gui.runCommand("QI_FinishSketch", 0)
|
|
settle(450)
|
|
assert commands._editing() is None
|
|
expected_volume = (60 * 25 - math.pi * 5**2) * 8
|
|
feature = model.pad(sketch, 8)
|
|
feature_name, sketch_name = feature.Name, sketch.Name
|
|
def verify_solid(candidate):
|
|
assert candidate is not None and candidate.TypeId == "PartDesign::Pad"
|
|
assert candidate.Shape.isValid() and len(candidate.Shape.Solids) == 1
|
|
assert abs(candidate.Shape.Volume - expected_volume) < 1e-5, (candidate.Shape.Volume, expected_volume)
|
|
return candidate.Shape.Volume
|
|
volume = verify_solid(feature)
|
|
undo_before = document.UndoCount
|
|
assert undo_before > 0
|
|
document.undo()
|
|
document.recompute()
|
|
assert document.getObject(feature_name) is None
|
|
assert document.getObject(sketch_name).GeometryCount == 5
|
|
assert document.RedoCount > 0
|
|
document.redo()
|
|
document.recompute()
|
|
redo_volume = verify_solid(document.getObject(feature_name))
|
|
project_path = os.path.join(output, "rectangle-with-hole-60x25-r5-pad8.FCStd")
|
|
model.save_project(document, project_path)
|
|
assert os.path.isfile(project_path) and os.path.getsize(project_path) > 0
|
|
with open(project_path, "rb") as stream:
|
|
project_sha = hashlib.sha256(stream.read()).hexdigest()
|
|
close_owned(document)
|
|
reopened = App.openDocument(project_path)
|
|
created.append(reopened.Name)
|
|
reopened.recompute()
|
|
reopened_volume = verify_solid(reopened.getObject(feature_name))
|
|
assert reopened.getObject(sketch_name).GeometryCount == 5
|
|
return {"sketch": sketch_evidence, "pad_creation": "qi_model.pad native PartDesign::Pad API",
|
|
"pad_length_mm": 8, "expected_volume_mm3": expected_volume, "volume_mm3": volume,
|
|
"undo_removed_pad": True, "redo_volume_mm3": redo_volume,
|
|
"saved_project": project_path, "saved_sha256": project_sha,
|
|
"reopened_volume_mm3": reopened_volume}
|
|
|
|
try:
|
|
commands.register_commands()
|
|
main.resize(1440, 940)
|
|
main.hide()
|
|
main.showNormal()
|
|
main.raise_()
|
|
main.activateWindow()
|
|
assert QtTest.QTest.qWaitForWindowExposed(main.windowHandle(), 5000)
|
|
ribbon = ui.show_ribbon()
|
|
set_preference("User parameter:BaseApp/Preferences/Mod/Sketcher/General", "RestoreCamera", True)
|
|
set_preference("User parameter:BaseApp/Preferences/Mod/Sketcher", "LeaveSketchWithEscape", False)
|
|
set_preference("User parameter:BaseApp/Preferences/Mod/Sketcher", "ShowDialogOnDistanceConstraint", True)
|
|
set_preference("User parameter:BaseApp/Preferences/Mod/Sketcher", "ContinuousCreationMode", True)
|
|
check("Native RestoreCamera survives QInventor Finish and Cancel without delayed changes", native_camera_roundtrip)
|
|
check("Empty native XY sketch enters the stable workspace and stays idle", empty_sketch)
|
|
check("Pinned Finish/Cancel and CAD viewport stay stable at two window sizes", pinned_controls)
|
|
check("Qt mouse input draws a real closed native rectangle from empty sketch", mouse_rectangle)
|
|
check("D creates 40x25 dimensions; the dimension list selects and edits the native datum", dimensions_and_list_edit)
|
|
check("Qt Circle and Radius menu create a hole; native Pad8 volume, Undo/Redo and FCStd reopening agree", mouse_hole_and_native_pad)
|
|
except Exception:
|
|
report["failed"] += 1
|
|
report["checks"].append({"name": "Workspace suite setup", "passed": False, "error": traceback.format_exc()})
|
|
finally:
|
|
for name in reversed(created):
|
|
if name in App.listDocuments():
|
|
try:
|
|
close_owned(App.getDocument(name))
|
|
except Exception:
|
|
report["failed"] += 1
|
|
report["checks"].append({"name": "Owned test document cleanup", "passed": False,
|
|
"document": name, "error": traceback.format_exc()})
|
|
for group, key, present, value in reversed(preferences):
|
|
group.SetBool(key, value) if present else group.RemBool(key)
|
|
if initial_active in App.listDocuments():
|
|
App.setActiveDocument(initial_active)
|
|
main.resize(previous_size)
|
|
report["status"] = "passed" if not report["failed"] else "failed"
|
|
write()
|
|
return report
|