942 lines
45 KiB
Python
942 lines
45 KiB
Python
"""Real FreeCAD/OCCT extrusion regression scenarios, usable headless or GUI."""
|
||
import json
|
||
import math
|
||
import os
|
||
import sys
|
||
import traceback
|
||
from pathlib import Path
|
||
|
||
import FreeCAD as App
|
||
import Part
|
||
import Sketcher
|
||
|
||
SOURCE = Path(__file__).resolve().parents[1] / "src" / "QInventor"
|
||
if str(SOURCE) not in sys.path:
|
||
sys.path.insert(0, str(SOURCE))
|
||
import qi_extrude as extrude
|
||
|
||
|
||
def _near(actual, expected, tolerance=1e-6):
|
||
if abs(actual - expected) > tolerance * max(1.0, abs(expected)):
|
||
raise AssertionError("Expected %.9g, obtained %.9g" % (expected, actual))
|
||
|
||
|
||
def _doc(name):
|
||
doc = App.newDocument(name)
|
||
doc.Label = "Проверка выдавливания"
|
||
doc.UndoMode = 1
|
||
body = doc.addObject("PartDesign::Body", "Body")
|
||
body.Label = "Тело"
|
||
return doc, body
|
||
|
||
|
||
def _rectangle(body, width=20, height=20, placement=None):
|
||
sk = body.newObject("Sketcher::SketchObject", "Sketch")
|
||
sk.Label = "Эскиз"
|
||
points = [App.Vector(0, 0, 0), App.Vector(width, 0, 0),
|
||
App.Vector(width, height, 0), App.Vector(0, height, 0)]
|
||
for i in range(4):
|
||
sk.addGeometry(Part.LineSegment(points[i], points[(i + 1) % 4]), False)
|
||
for i in range(4):
|
||
sk.addConstraint(Sketcher.Constraint("Coincident", i, 2, (i + 1) % 4, 1))
|
||
for i in (0, 2):
|
||
sk.addConstraint(Sketcher.Constraint("Horizontal", i))
|
||
for i in (1, 3):
|
||
sk.addConstraint(Sketcher.Constraint("Vertical", i))
|
||
sk.addConstraint(Sketcher.Constraint("Coincident", 0, 1, -1, 1))
|
||
width_index = sk.addConstraint(Sketcher.Constraint("Distance", 0, float(width)))
|
||
height_index = sk.addConstraint(Sketcher.Constraint("Distance", 1, float(height)))
|
||
sk.renameConstraint(width_index, "Width")
|
||
sk.renameConstraint(height_index, "Height")
|
||
if placement is not None:
|
||
sk.Placement = placement
|
||
body.Document.recompute()
|
||
return sk
|
||
|
||
|
||
def _circle(body, radius=3, z=0, x=0, y=0):
|
||
sk = body.newObject("Sketcher::SketchObject", "SketchCircle")
|
||
sk.Label = "Эскиз окружности"
|
||
sk.addGeometry(Part.Circle(App.Vector(0, 0, 0), App.Vector(0, 0, 1), radius), False)
|
||
index = sk.addConstraint(Sketcher.Constraint("Radius", 0, radius))
|
||
sk.renameConstraint(index, "Radius")
|
||
sk.Placement = App.Placement(App.Vector(x, y, z), App.Rotation())
|
||
body.Document.recompute()
|
||
return sk, index
|
||
|
||
|
||
def _baseline(name):
|
||
doc, body = _doc(name)
|
||
sk = _rectangle(body)
|
||
pad = body.newObject("PartDesign::Pad", "Pad")
|
||
pad.Profile, pad.Length = sk, 10
|
||
body.Tip = pad
|
||
doc.recompute()
|
||
_near(pad.Shape.Volume, 4000)
|
||
return doc, body, pad
|
||
|
||
|
||
def _plane(doc, z=10, slope=0, size=100, placement=None):
|
||
obj = doc.addObject("Part::Feature", "TerminationPlane")
|
||
obj.Label = "Конечная плоскость"
|
||
obj.Shape = Part.makePlane(size, size, App.Vector(-size / 2, -size / 2, 0))
|
||
obj.Placement = placement or App.Placement(App.Vector(0, 0, z),
|
||
App.Rotation(App.Vector(0, 1, 0), -math.degrees(math.atan(slope))))
|
||
doc.recompute()
|
||
return obj
|
||
|
||
|
||
def run(output_dir):
|
||
output = Path(output_dir)
|
||
output.mkdir(parents=True, exist_ok=True)
|
||
if App.GuiUp:
|
||
# Exercise the product's actual Russian native labels as well as our
|
||
# task form; the test uses its own isolated user-config file.
|
||
import qi_localization
|
||
qi_localization.install()
|
||
results = []
|
||
documents = []
|
||
|
||
def case(name, action):
|
||
before = set(App.listDocuments())
|
||
try:
|
||
details = action() or {}
|
||
results.append({"name": name, "passed": True, **details})
|
||
except Exception as exc:
|
||
results.append({"name": name, "passed": False, "error": str(exc),
|
||
"traceback": traceback.format_exc()})
|
||
finally:
|
||
for created in set(App.listDocuments()) - before:
|
||
App.closeDocument(created)
|
||
|
||
def oriented(direction, rotation, lengths):
|
||
doc, body = _doc("ExtrudePlane" + direction)
|
||
documents.append(doc.Name)
|
||
sk = _rectangle(body, 10, 6, App.Placement(App.Vector(), rotation))
|
||
obj = extrude.create_extrusion(sk, 4, direction=direction)
|
||
_near(obj.Shape.Volume, 240)
|
||
bbox = obj.Shape.BoundBox
|
||
for actual, expected in zip((bbox.XLength, bbox.YLength, bbox.ZLength), lengths):
|
||
_near(actual, expected)
|
||
if rotation.Angle == 0:
|
||
_near(bbox.ZMin, {"Forward": 0, "Reverse": -4, "Symmetric": -2}[direction])
|
||
return {"volume": obj.Shape.Volume, "bbox": [bbox.XLength, bbox.YLength, bbox.ZLength]}
|
||
|
||
case("XY forward", lambda: oriented("Forward", App.Rotation(), (10, 6, 4)))
|
||
case("XY reverse", lambda: oriented("Reverse", App.Rotation(), (10, 6, 4)))
|
||
case("XY symmetric", lambda: oriented("Symmetric", App.Rotation(), (10, 6, 4)))
|
||
case("XZ plane placement", lambda: oriented("Forward", App.Rotation(App.Vector(1, 0, 0), 90), (10, 4, 6)))
|
||
case("YZ plane placement", lambda: oriented("Forward", App.Rotation(App.Vector(0, 1, 0), 90), (4, 6, 10)))
|
||
|
||
def annulus():
|
||
doc, body = _doc("ExtrudeAnnulus")
|
||
documents.append(doc.Name)
|
||
sk, _ = _circle(body, 3)
|
||
sk.addGeometry(Part.Circle(App.Vector(), App.Vector(0, 0, 1), 10), False)
|
||
doc.recompute()
|
||
obj = extrude.create_extrusion(sk, 4)
|
||
_near(obj.Shape.Volume, math.pi * 91 * 4)
|
||
assert len(obj.Shape.Solids) == 1
|
||
return {"volume": obj.Shape.Volume}
|
||
case("Nested wires preserve hole", annulus)
|
||
|
||
def parametric():
|
||
doc, body = _doc("ExtrudeParametric")
|
||
documents.append(doc.Name)
|
||
sk, radius_index = _circle(body, 5)
|
||
obj = extrude.create_extrusion(sk, 2)
|
||
_near(obj.Shape.Volume, math.pi * 50)
|
||
sk.setDatum(radius_index, App.Units.Quantity("8 mm"))
|
||
doc.recompute()
|
||
_near(obj.Shape.Volume, math.pi * 128)
|
||
obj.setExpression("Length", "3 mm")
|
||
doc.recompute()
|
||
_near(obj.Shape.Volume, math.pi * 192)
|
||
path = output / "parametric-extrusion.FCStd"
|
||
doc.saveAs(str(path))
|
||
doc_name, feature_name = doc.Name, obj.Name
|
||
App.closeDocument(doc_name)
|
||
documents.remove(doc_name)
|
||
reopened = App.openDocument(str(path))
|
||
documents.append(reopened.Name)
|
||
restored = reopened.getObject(feature_name)
|
||
assert isinstance(restored.Proxy, extrude.ExtrusionProxy)
|
||
restored.Profile.setDatum(radius_index, App.Units.Quantity("9 mm"))
|
||
reopened.recompute()
|
||
_near(restored.Shape.Volume, math.pi * 243)
|
||
return {"volumeAfterReopenAndEdit": restored.Shape.Volume, "file": str(path)}
|
||
case("Sketch edits, length expressions and FCStd reopen", parametric)
|
||
|
||
def boolean(operation, extent="Distance", direction="Forward"):
|
||
doc, body, pad = _baseline("Extrude" + operation.replace(" ", "") + extent.replace(" ", ""))
|
||
documents.append(doc.Name)
|
||
if operation == "Join":
|
||
sk = _rectangle(body, 10, 10, App.Placement(App.Vector(0, 0, 10), App.Rotation()))
|
||
length, expected = 5, 4500
|
||
elif operation == "Cut":
|
||
sk, _ = _circle(body, 3, z=10, x=10, y=10)
|
||
length = 3
|
||
expected = 4000 - math.pi * 9 * (10 if extent == "Through all" else 3)
|
||
else:
|
||
sk = _rectangle(body, 10, 10, App.Placement(App.Vector(5, 5, 0), App.Rotation()))
|
||
length, expected = 4, 400
|
||
# Creating an unconsumed sketch must preserve the preceding solid Tip.
|
||
assert body.Tip == pad
|
||
obj = extrude.create_extrusion(sk, length, operation, direction, extent)
|
||
assert obj.BaseFeature == pad
|
||
assert body.Tip == obj
|
||
_near(obj.Shape.Volume, expected)
|
||
return {"volume": obj.Shape.Volume, "base": obj.BaseFeature.Name}
|
||
case("Join against previous solid Body Tip", lambda: boolean("Join"))
|
||
case("Cut reverse against previous solid Body Tip", lambda: boolean("Cut", direction="Reverse"))
|
||
case("Cut through all", lambda: boolean("Cut", "Through all", "Reverse"))
|
||
case("True OCCT intersection", lambda: boolean("Intersect"))
|
||
|
||
def through_recompute():
|
||
doc, body, pad = _baseline("ExtrudeThroughRecompute")
|
||
documents.append(doc.Name)
|
||
sk, _ = _circle(body, 3, x=10, y=10)
|
||
obj = extrude.create_extrusion(sk, 1, "Cut", "Symmetric", "Through all")
|
||
pad.Length = 16
|
||
doc.recompute()
|
||
_near(obj.Shape.Volume, 6400 - math.pi * 9 * 16)
|
||
return {"volumeAfterBaseChange": obj.Shape.Volume}
|
||
case("Through all recalculates extent after base change", through_recompute)
|
||
|
||
def separate():
|
||
doc, body, pad = _baseline("ExtrudeSeparate")
|
||
documents.append(doc.Name)
|
||
body.Placement = App.Placement(App.Vector(2, 3, 4), App.Rotation())
|
||
sk, _ = _circle(body, 3, x=50)
|
||
obj = extrude.create_extrusion(sk, 5, "New Solid")
|
||
target_body = extrude._body_of(obj)
|
||
assert target_body != body
|
||
assert obj.BaseFeature is None
|
||
assert pad in body.Group
|
||
assert body.Tip == pad
|
||
_near(obj.Shape.Volume, math.pi * 45)
|
||
_near(target_body.Placement.Base.x, 2)
|
||
_near(target_body.Shape.BoundBox.Center.x, 52)
|
||
_near(target_body.Shape.BoundBox.Center.y, 3)
|
||
_near(target_body.Shape.BoundBox.Center.z, 6.5)
|
||
return {"bodies": len([x for x in doc.Objects if x.isDerivedFrom("PartDesign::Body")])}
|
||
case("New Solid is independent and preserves body placement", separate)
|
||
|
||
def separate_container():
|
||
doc, body, pad = _baseline("ExtrudeSeparateContainer")
|
||
container = doc.addObject("App::Part", "Container")
|
||
container.Label = "Деталь с несколькими телами"
|
||
container.Placement = App.Placement(App.Vector(30, 40, 50), App.Rotation(App.Vector(0, 0, 1), 90))
|
||
container.addObject(body)
|
||
body.Placement = App.Placement(App.Vector(2, 3, 4), App.Rotation())
|
||
sk, _ = _circle(body, 3, x=50)
|
||
obj = extrude.create_extrusion(sk, 5, "New Solid")
|
||
new_body = extrude._body_of(obj)
|
||
assert new_body in container.Group and body in container.Group
|
||
_near(new_body.Placement.Base.x, 2)
|
||
_near(new_body.getGlobalPlacement().Base.x, 27)
|
||
_near(new_body.getGlobalPlacement().Base.y, 42)
|
||
_near(new_body.getGlobalPlacement().Base.z, 54)
|
||
_near(obj.Shape.Volume, math.pi * 45)
|
||
assert body.Tip == pad and extrude._is_solid(body) and extrude._is_solid(new_body)
|
||
return {"samePartSiblingAndLocalPlacementPreserved": True}
|
||
case("New Solid remains a sibling inside transformed App Part", separate_container)
|
||
|
||
def invalid():
|
||
doc, body = _doc("ExtrudeInvalid")
|
||
documents.append(doc.Name)
|
||
sk = body.newObject("Sketcher::SketchObject", "OpenSketch")
|
||
sk.addGeometry(Part.LineSegment(App.Vector(), App.Vector(4, 0, 0)), False)
|
||
doc.recompute()
|
||
original = set(obj.Name for obj in doc.Objects)
|
||
for value in (0, -1, float("inf"), float("nan"), 4):
|
||
try:
|
||
extrude.create_extrusion(sk, value)
|
||
except ValueError:
|
||
pass
|
||
else:
|
||
raise AssertionError("Invalid input accepted")
|
||
assert original == set(obj.Name for obj in doc.Objects)
|
||
return {"objectsUnchanged": True}
|
||
case("Open profile and invalid lengths leave document unchanged", invalid)
|
||
|
||
def disjoint():
|
||
doc, body, pad = _baseline("ExtrudeDisjoint")
|
||
documents.append(doc.Name)
|
||
sk, _ = _circle(body, 3, x=100)
|
||
original = set(obj.Name for obj in doc.Objects)
|
||
for operation in ("Join", "Cut", "Intersect"):
|
||
try:
|
||
extrude.create_extrusion(sk, 4, operation)
|
||
except ValueError:
|
||
pass
|
||
else:
|
||
raise AssertionError("Disjoint %s should be rejected" % operation)
|
||
assert original == set(obj.Name for obj in doc.Objects)
|
||
return {"objectsUnchanged": True}
|
||
case("Disjoint booleans are rejected before mutation", disjoint)
|
||
|
||
def undo():
|
||
doc, body = _doc("ExtrudeUndo")
|
||
documents.append(doc.Name)
|
||
sk = _rectangle(body, 10, 6)
|
||
initial_tip = body.Tip
|
||
obj = extrude.create_extrusion(sk, 4)
|
||
name = obj.Name
|
||
doc.undo()
|
||
doc.recompute()
|
||
assert doc.getObject(name) is None
|
||
assert body.Tip == initial_tip
|
||
doc.redo()
|
||
doc.recompute()
|
||
_near(doc.getObject(name).Shape.Volume, 240)
|
||
return {"undoRedo": True}
|
||
case("Native undo redo retains geometry", undo)
|
||
|
||
def asymmetric():
|
||
doc, body = _doc("ExtrudeAsymmetric")
|
||
sk = _rectangle(body, 10, 6)
|
||
obj = extrude.create_extrusion(sk, 4, direction="Asymmetric", length2=7)
|
||
_near(obj.Shape.Volume, 660)
|
||
_near(obj.Shape.BoundBox.ZMin, -7)
|
||
_near(obj.Shape.BoundBox.ZMax, 4)
|
||
assert str(obj.Direction) == "Асимметрично"
|
||
obj.setExpression("Length2", "2 * Length")
|
||
doc.recompute()
|
||
_near(obj.Shape.Volume, 720)
|
||
doc.openTransaction("Изменить расстояния")
|
||
obj.Length = 6
|
||
doc.recompute()
|
||
doc.commitTransaction()
|
||
_near(obj.Shape.Volume, 1080)
|
||
doc.undo()
|
||
doc.recompute()
|
||
_near(obj.Shape.Volume, 720)
|
||
path = output / "asymmetric-extrusion.FCStd"
|
||
doc.saveAs(str(path))
|
||
name = obj.Name
|
||
App.closeDocument(doc.Name)
|
||
reopened = App.openDocument(str(path))
|
||
restored = reopened.getObject(name)
|
||
assert str(restored.Direction) == "Асимметрично"
|
||
restored.Length = 5
|
||
reopened.recompute()
|
||
_near(restored.Shape.Volume, 900)
|
||
return {"length2ExpressionUndoAndReopen": True, "volumeAfterReopen": restored.Shape.Volume}
|
||
case("Asymmetric persisted distances, expressions, undo and reopen", asymmetric)
|
||
|
||
def to_face(slope=0, reverse=False):
|
||
doc, body = _doc("ExtrudeToFace")
|
||
sk = _rectangle(body, 10, 6)
|
||
target = _plane(doc, -10 if reverse else 10, -slope if reverse else slope)
|
||
obj = extrude.create_extrusion(sk, 1, direction="Reverse" if reverse else "Forward",
|
||
extent="To face", target_face=(target, ["Face1"]))
|
||
expected = 60 * (10 + 5 * slope)
|
||
_near(obj.Shape.Volume, expected)
|
||
_near(body.Shape.Volume, expected)
|
||
assert obj.TargetFace[0] == target and list(obj.TargetFace[1]) == ["Face1"]
|
||
assert obj.getTypeIdOfProperty("TargetFace") == "App::PropertyLinkSubGlobal"
|
||
assert str(obj.Extent) == "До грани"
|
||
caps = [face for face in obj.Shape.Faces if isinstance(face.Surface, Part.Plane)
|
||
and abs(face.Surface.Axis.dot(target.Shape.Faces[0].Surface.Axis)) > 1 - 1e-8]
|
||
assert any(abs(face.common(target.Shape).Area - face.Area) < 1e-7 for face in caps)
|
||
target.Placement.Base.z += -2 if reverse else 2
|
||
doc.recompute()
|
||
_near(obj.Shape.Volume, expected + 120)
|
||
return {"slope": slope, "reverse": reverse, "volumeAfterPlaneMove": obj.Shape.Volume}
|
||
case("To face planar termination and support movement", to_face)
|
||
case("To face exact oblique termination", lambda: to_face(0.5))
|
||
case("To face exact oblique reverse termination", lambda: to_face(0.5, True))
|
||
|
||
def to_face_hole():
|
||
doc, body = _doc("ExtrudeToFaceHole")
|
||
sk, _ = _circle(body, 3)
|
||
sk.addGeometry(Part.Circle(App.Vector(), App.Vector(0, 0, 1), 10), False)
|
||
target = _plane(doc, 20, 0.5)
|
||
obj = extrude.create_extrusion(sk, 1, extent="To face", target_face=(target, "Face1"))
|
||
_near(obj.Shape.Volume, math.pi * 91 * 20)
|
||
assert len(obj.Shape.Solids) == 1
|
||
return {"obliqueAnnulusVolume": obj.Shape.Volume}
|
||
case("To face exact oblique cap preserves nested holes", to_face_hole)
|
||
|
||
def extend_face():
|
||
doc, body = _doc("ExtrudeExtendFace")
|
||
sk = _rectangle(body, 10, 6)
|
||
target = _plane(doc, 10, 0.5, size=2)
|
||
original = set(obj.Name for obj in doc.Objects)
|
||
try:
|
||
extrude.create_extrusion(sk, 1, extent="To face", target_face=(target, ["Face1"]))
|
||
except ValueError as exc:
|
||
assert "Продолжить грань" in str(exc)
|
||
else:
|
||
raise AssertionError("A finite face covering only part of the profile was accepted")
|
||
assert original == set(obj.Name for obj in doc.Objects)
|
||
obj = extrude.create_extrusion(sk, 1, extent="To face", target_face=(target, ["Face1"]), extend_face=True)
|
||
_near(obj.Shape.Volume, 750)
|
||
assert obj.ExtendFace
|
||
return {"finiteBoundaryRejected": True, "extendedPlaneVolume": obj.Shape.Volume}
|
||
case("To face finite support coverage and explicit extension", extend_face)
|
||
|
||
def cut_to_face():
|
||
doc, body, pad = _baseline("ExtrudeCutToFace")
|
||
sk, _ = _circle(body, 3, z=10, x=10, y=10)
|
||
bottom = next("Face%d" % (index + 1) for index, face in enumerate(pad.Shape.Faces)
|
||
if isinstance(face.Surface, Part.Plane) and abs(face.CenterOfMass.z) < 1e-7
|
||
and abs(face.Surface.Axis.z) > 0.9)
|
||
# Native viewport selection may name the Body instead of its Tip.
|
||
obj = extrude.create_extrusion(sk, 1, "Cut", "Reverse", "To face", target_face=(body, [bottom]))
|
||
_near(obj.Shape.Volume, 4000 - math.pi * 90)
|
||
assert obj.TargetFace[0] == obj.BaseFeature == pad
|
||
assert extrude._is_solid(body)
|
||
assert not obj.SupportObjects
|
||
doc.undo()
|
||
doc.recompute()
|
||
assert body.Tip == pad
|
||
doc.redo()
|
||
doc.recompute()
|
||
_near(body.Tip.Shape.Volume, 4000 - math.pi * 90)
|
||
return {"sameBodyFaceReferenceWithoutDependencyCycle": True}
|
||
case("To face cut references previous solid and survives undo redo", cut_to_face)
|
||
|
||
def holed_target_boundary():
|
||
doc, body = _doc("ExtrudeHoledTarget")
|
||
sk = _rectangle(body, 10, 6)
|
||
target = _plane(doc, 10)
|
||
hole = Part.Wire(Part.makeCircle(1, App.Vector(5, 3, 10)))
|
||
inner = Part.Face(hole)
|
||
target.Shape = target.Shape.cut(inner)
|
||
doc.recompute()
|
||
_near(target.Shape.Area, 10000 - math.pi)
|
||
original = set(item.Name for item in doc.Objects)
|
||
try:
|
||
extrude.create_extrusion(sk, 1, extent="To face", target_face=(target, ["Face1"]))
|
||
except ValueError as exc:
|
||
assert "не перекрывает" in str(exc), str(exc)
|
||
else:
|
||
raise AssertionError("A hole in the finite termination face was ignored")
|
||
assert original == set(item.Name for item in doc.Objects)
|
||
obj = extrude.create_extrusion(sk, 1, extent="To face", target_face=(target, ["Face1"]), extend_face=True)
|
||
_near(obj.Shape.Volume, 600)
|
||
return {"terminationFaceHoleRespected": True}
|
||
case("To face finite boundary honors holes in termination support", holed_target_boundary)
|
||
|
||
def target_placement():
|
||
doc, body = _doc("ExtrudeTargetPlacement")
|
||
transform = App.Placement(App.Vector(50, 30, 20), App.Rotation(App.Vector(1, 0, 0), 35))
|
||
body.Placement = transform
|
||
sk = _rectangle(body, 10, 6)
|
||
container = doc.addObject("App::Part", "TargetContainer")
|
||
container.Label = "Опора"
|
||
container.Placement = transform
|
||
target = _plane(doc, 10, 0.5)
|
||
container.addObject(target)
|
||
doc.recompute()
|
||
obj = extrude.create_extrusion(sk, 1, extent="To face", target_face=(target, ["Face1"]))
|
||
_near(obj.Shape.Volume, 750)
|
||
assert extrude._is_solid(body)
|
||
path = output / "to-face-extrusion.FCStd"
|
||
doc.saveAs(str(path))
|
||
feature_name, target_name, container_name = obj.Name, target.Name, container.Name
|
||
App.closeDocument(doc.Name)
|
||
reopened = App.openDocument(str(path))
|
||
restored = reopened.getObject(feature_name)
|
||
assert restored.TargetFace[0] == reopened.getObject(target_name)
|
||
assert str(restored.Extent) == "До грани"
|
||
reopened.getObject(target_name).Placement.Base.z += 2
|
||
reopened.recompute()
|
||
_near(restored.Shape.Volume, 870)
|
||
# The persisted LinkSub must observe parent-container placement changes.
|
||
reopened.getObject(container_name).Placement.Base.z += 2
|
||
reopened.recompute()
|
||
# The moved support is tilted relative to global z: local offset is
|
||
# 2*cos(35 deg), with no local x component for this X-axis rotation.
|
||
_near(restored.Shape.Volume, 870 + 120 * math.cos(math.radians(35)))
|
||
return {"globalPlacementsAndReopen": True, "volumeAfterReopen": restored.Shape.Volume}
|
||
case("To face across transformed containers and FCStd reopen", target_placement)
|
||
|
||
def invalid_targets():
|
||
doc, body = _doc("ExtrudeInvalidTargets")
|
||
sk = _rectangle(body, 10, 6)
|
||
behind = _plane(doc, -10)
|
||
crossing = _plane(doc, -2, 0.5)
|
||
parallel = _plane(doc, placement=App.Placement(App.Vector(10, 0, 0), App.Rotation(App.Vector(0, 1, 0), 90)))
|
||
curved = doc.addObject("Part::Feature", "CurvedTarget")
|
||
curved.Label = "Цилиндрическая опора"
|
||
curved.Shape = Part.makeCylinder(20, 10, App.Vector(0, 0, 10))
|
||
doc.recompute()
|
||
original = set(obj.Name for obj in doc.Objects)
|
||
for link in (None, (behind, ["Face1"]), (crossing, ["Face1"]),
|
||
(parallel, ["Face1"]), (behind, ["Edge1"]),
|
||
(behind, ["Face999"]), (curved, ["Face1"])):
|
||
try:
|
||
extrude.create_extrusion(sk, 1, extent="To face", target_face=link)
|
||
except ValueError as exc:
|
||
assert any("А" <= char <= "я" for char in str(exc)), str(exc)
|
||
else:
|
||
raise AssertionError("Invalid target accepted: %s" % (None if not link else (link[0].Name, link[1]),))
|
||
assert original == set(obj.Name for obj in doc.Objects)
|
||
for direction, extent, second in (("Asymmetric", "Distance", -2), ("Asymmetric", "Through all", 2),
|
||
("Symmetric", "To face", 2), ("Invalid", "Distance", 2)):
|
||
try:
|
||
extrude.create_extrusion(sk, 1, direction=direction, extent=extent, length2=second,
|
||
target_face=(behind, ["Face1"]))
|
||
except ValueError:
|
||
pass
|
||
else:
|
||
raise AssertionError("Invalid direction or second length accepted")
|
||
assert original == set(obj.Name for obj in doc.Objects)
|
||
return {"invalidTargetsAndDirectionsLeaveDocumentUnchanged": True}
|
||
case("Invalid target surfaces, directions and second length rejected in Russian", invalid_targets)
|
||
|
||
def to_next():
|
||
doc, body, pad = _baseline("ExtrudeToNext")
|
||
sk, _ = _circle(body, 3, z=10, x=10, y=10)
|
||
obj = extrude.create_extrusion(sk, 1, "Cut", "Reverse", "To next", terminator=body)
|
||
_near(obj.Shape.Volume, 4000 - math.pi * 90)
|
||
assert obj.Terminator == pad
|
||
assert str(obj.Extent) == "До следующей"
|
||
pad.Length = 15
|
||
doc.recompute()
|
||
# Sketch remains at z10; backward cut terminates at the bottom z0.
|
||
_near(obj.Shape.Volume, 6000 - math.pi * 90)
|
||
return {"nextWholePlanarFace": True, "volumeAfterBaseChange": obj.Shape.Volume}
|
||
case("To next true planar termination on preceding solid", to_next)
|
||
|
||
def next_selected_terminator():
|
||
doc, body, pad = _baseline("ExtrudeSelectedNext")
|
||
sk = _rectangle(body, 10, 6, App.Placement(App.Vector(0, 0, 10), App.Rotation()))
|
||
target = doc.addObject("Part::Feature", "TerminationSolid")
|
||
target.Label = "Тело завершения"
|
||
target.Shape = Part.makeBox(20, 20, 5, App.Vector(-1, -1, 20))
|
||
doc.recompute()
|
||
obj = extrude.create_extrusion(sk, 1, extent="To next", terminator=target)
|
||
_near(obj.Shape.Volume, 4600)
|
||
assert obj.Terminator == target
|
||
target.Placement.Base.z = 3
|
||
doc.recompute()
|
||
_near(obj.Shape.Volume, 4780)
|
||
return {"selectedTerminatorRegenerates": True, "volume": obj.Shape.Volume}
|
||
case("To next explicit terminator tracks nearest complete face", next_selected_terminator)
|
||
|
||
def next_ambiguous():
|
||
doc, body, pad = _baseline("ExtrudeAmbiguousNext")
|
||
sk = _rectangle(body, 10, 6, App.Placement(App.Vector(0, 0, 10), App.Rotation()))
|
||
target = doc.addObject("Part::Feature", "AmbiguousPlanes")
|
||
target.Shape = Part.makeCompound([_plane(doc, 20, 0.5).Shape, _plane(doc, 25, -0.5).Shape])
|
||
# The two finite supporting faces swap order at x5.
|
||
doc.recompute()
|
||
original = set(obj.Name for obj in doc.Objects)
|
||
try:
|
||
extrude.create_extrusion(sk, 1, extent="To next", terminator=target)
|
||
except ValueError as exc:
|
||
assert "неоднозначна" in str(exc), str(exc)
|
||
else:
|
||
raise AssertionError("Crossing termination planes accepted as one nearest face")
|
||
assert original == set(obj.Name for obj in doc.Objects)
|
||
return {"ambiguousCrossingPlanesRejected": True}
|
||
case("To next rejects ambiguous crossing support planes", next_ambiguous)
|
||
|
||
def next_curved_rejected():
|
||
doc, body, pad = _baseline("ExtrudeCurvedNext")
|
||
sk = _rectangle(body, 10, 6, App.Placement(App.Vector(0, 0, 10), App.Rotation()))
|
||
target = doc.addObject("Part::Feature", "CurvedTerminator")
|
||
target.Shape = Part.makeCylinder(20, 10, App.Vector(5, 3, 20))
|
||
doc.recompute()
|
||
original = set(item.Name for item in doc.Objects)
|
||
try:
|
||
extrude.create_extrusion(sk, 1, extent="To next", terminator=target)
|
||
except ValueError as exc:
|
||
assert "криволинейной" in str(exc)
|
||
else:
|
||
raise AssertionError("Curved next-face terminator accepted")
|
||
assert original == set(item.Name for item in doc.Objects)
|
||
return {"curvedNextLimitationExplicit": True}
|
||
case("To next explicitly rejects curved terminators", next_curved_rejected)
|
||
|
||
def change_operation_bodies():
|
||
doc, body, pad = _baseline("ExtrudeOperationBodyEdit")
|
||
sk, _ = _circle(body, 3, z=10, x=10, y=10)
|
||
obj = extrude.create_extrusion(sk, 3)
|
||
doc.openTransaction("Перевести в новое тело")
|
||
extrude._update_feature(obj, (sk, 3, "New Solid", "Forward", "Distance", pad, 10, None, False, None))
|
||
doc.commitTransaction()
|
||
new_body_name = extrude._body_of(obj).Name
|
||
assert extrude._body_of(obj) != body and body.Tip == pad
|
||
_near(obj.Shape.Volume, math.pi * 27)
|
||
_near(body.Shape.Volume, 4000)
|
||
assert obj.BaseFeature is None
|
||
doc.undo()
|
||
doc.recompute()
|
||
assert body.Tip == obj and extrude._body_of(obj) == body
|
||
assert doc.getObject(new_body_name) is None
|
||
_near(obj.Shape.Volume, 4000 + math.pi * 27)
|
||
doc.redo()
|
||
doc.recompute()
|
||
assert body.Tip == pad and extrude._body_of(obj) != body
|
||
doc.openTransaction("Вернуть вырезание в исходное тело")
|
||
base = extrude._edit_base(obj)
|
||
assert base == pad
|
||
extrude._update_feature(obj, (sk, 3, "Cut", "Reverse", "Distance", base, 10, None, False, None))
|
||
doc.commitTransaction()
|
||
assert extrude._body_of(obj) == body and body.Tip == obj
|
||
assert doc.getObject(new_body_name) is None
|
||
_near(obj.Shape.Volume, 4000 - math.pi * 27)
|
||
doc.undo()
|
||
doc.recompute()
|
||
assert body.Tip == pad and extrude._body_of(obj) != body
|
||
_near(obj.Shape.Volume, math.pi * 27)
|
||
return {"operationChangeMovesFeatureAndUndoRestoresBodies": True}
|
||
case("Changing operation moves real history between solid bodies with undo redo", change_operation_bodies)
|
||
|
||
if App.GuiUp:
|
||
def preview_cancel():
|
||
doc, body, pad = _baseline("ExtrudePreviewCancel")
|
||
documents.append(doc.Name)
|
||
sk = _rectangle(body, 10, 10, App.Placement(App.Vector(0, 0, 10), App.Rotation()))
|
||
initial = set(obj.Name for obj in doc.Objects)
|
||
visible = pad.Visibility
|
||
panel = extrude.ExtrusionTaskPanel(doc, sk)
|
||
assert panel._preview is not None
|
||
_near(panel._preview.Shape.Volume, 5000)
|
||
panel.length.setValue(5)
|
||
panel.update_preview()
|
||
_near(panel._preview.Shape.Volume, 4500)
|
||
assert panel.reject()
|
||
assert initial == set(obj.Name for obj in doc.Objects)
|
||
assert pad.Visibility == visible
|
||
return {"previewDeleted": True, "visibilityRestored": True}
|
||
case("GUI real preview update and cancel rollback", preview_cancel)
|
||
|
||
def preview_accept():
|
||
doc, body, pad = _baseline("ExtrudePreviewAccept")
|
||
documents.append(doc.Name)
|
||
sk = _rectangle(body, 10, 10, App.Placement(App.Vector(0, 0, 10), App.Rotation()))
|
||
panel = extrude.ExtrusionTaskPanel(doc, sk)
|
||
panel.length.setValue(5)
|
||
assert panel.accept(), panel.status.text()
|
||
obj = body.Tip
|
||
_near(obj.Shape.Volume, 4500)
|
||
assert not any(x.Name.startswith("QI_ExtrudePreview") for x in doc.Objects)
|
||
assert not pad.Visibility
|
||
doc.undo()
|
||
doc.recompute()
|
||
assert not any(x.Name.startswith("QI_Extrusion") for x in doc.Objects)
|
||
assert not any(x.Name.startswith("QI_ExtrudePreview") for x in doc.Objects)
|
||
doc.redo()
|
||
doc.recompute()
|
||
obj = body.Tip
|
||
_near(obj.Shape.Volume, 4500)
|
||
editor = extrude.ExtrusionTaskPanel(doc, obj.Profile, obj)
|
||
editor.length.setValue(7)
|
||
assert editor.accept(), editor.status.text()
|
||
_near(obj.Shape.Volume, 4700)
|
||
doc.undo()
|
||
doc.recompute()
|
||
_near(obj.Shape.Volume, 4500)
|
||
return {"acceptPreviewDeleted": True, "editUndo": True}
|
||
case("GUI accept, parameter edit and undo", preview_accept)
|
||
|
||
def preview_placement():
|
||
doc, body = _doc("ExtrudePreviewPlacement")
|
||
documents.append(doc.Name)
|
||
body.Placement = App.Placement(App.Vector(2, 3, 4), App.Rotation())
|
||
sk = _rectangle(body, 10, 6,
|
||
App.Placement(App.Vector(0, 0, 10), App.Rotation(App.Vector(1, 0, 0), 90)))
|
||
panel = extrude.ExtrusionTaskPanel(doc, sk)
|
||
panel.length.setValue(4)
|
||
panel.update_preview()
|
||
bb = panel._preview.Shape.BoundBox
|
||
for actual, expected in zip((bb.XLength, bb.YLength, bb.ZLength), (10, 4, 6)):
|
||
_near(actual, expected)
|
||
for actual, expected in zip((bb.XMin, bb.YMin, bb.ZMin), (2, -1, 14)):
|
||
_near(actual, expected)
|
||
panel.reject()
|
||
return {"rotatedSketchAndTranslatedBody": True}
|
||
case("GUI preview respects sketch and body placements", preview_placement)
|
||
|
||
def task_dialog():
|
||
import FreeCADGui as Gui
|
||
from PySide import QtCore
|
||
doc, body = _doc("ExtrudeTaskDialog")
|
||
documents.append(doc.Name)
|
||
sk = _rectangle(body, 10, 6)
|
||
Gui.Selection.clearSelection()
|
||
Gui.Selection.addSelection(sk)
|
||
panel = extrude.show_dialog()
|
||
assert isinstance(panel.getStandardButtons(), int)
|
||
QtCore.QCoreApplication.processEvents()
|
||
assert Gui.Control.activeDialog()
|
||
assert panel.profiles.currentData() == sk.Name
|
||
_, _, QtWidgets = extrude._gui_modules()
|
||
main = Gui.getMainWindow()
|
||
main.resize(1440, 960)
|
||
mdi = main.findChild(QtWidgets.QMdiArea)
|
||
if mdi and mdi.activeSubWindow():
|
||
mdi.activeSubWindow().showMaximized()
|
||
for dock in main.findChildren(QtWidgets.QDockWidget):
|
||
if dock.objectName() == "Tasks":
|
||
main.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock)
|
||
dock.show()
|
||
dock.raise_()
|
||
Gui.activeDocument().activeView().viewAxonometric()
|
||
Gui.activeDocument().activeView().fitAll()
|
||
try:
|
||
from PySide6 import QtTest
|
||
except ImportError:
|
||
from PySide2 import QtTest
|
||
QtTest.QTest.qWait(250)
|
||
assert panel.form.isVisible(), "Extrusion task form is hidden"
|
||
assert panel.form.width() > 120 and panel.form.height() > 120
|
||
image_path = output / "extrusion-task-panel.png"
|
||
main.grab().save(str(image_path))
|
||
boxes = [box for box in main.findChildren(QtWidgets.QDialogButtonBox) if box.isVisible()]
|
||
assert boxes, "Native task buttons are hidden"
|
||
boxes[0].button(QtWidgets.QDialogButtonBox.Cancel).click()
|
||
QtTest.QTest.qWait(50)
|
||
assert not Gui.Control.activeDialog()
|
||
assert not any(x.Name.startswith("QI_ExtrudePreview") for x in doc.Objects)
|
||
panel = extrude.show_dialog()
|
||
QtTest.QTest.qWait(50)
|
||
boxes = [box for box in main.findChildren(QtWidgets.QDialogButtonBox) if box.isVisible()]
|
||
assert boxes
|
||
boxes[0].button(QtWidgets.QDialogButtonBox.Ok).click()
|
||
QtTest.QTest.qWait(50)
|
||
assert not Gui.Control.activeDialog()
|
||
_near(body.Tip.Shape.Volume, 600)
|
||
assert not any(x.Name.startswith("QI_ExtrudePreview") for x in doc.Objects)
|
||
feature = body.Tip
|
||
assert Gui.activeDocument().setEdit(feature.Name)
|
||
QtTest.QTest.qWait(50)
|
||
assert Gui.Control.activeDialog()
|
||
editor = main.findChild(QtWidgets.QDoubleSpinBox, "QI_ExtrudeLength")
|
||
assert editor is not None and editor.isVisible()
|
||
editor.setValue(15)
|
||
QtTest.QTest.qWait(100)
|
||
boxes = [box for box in main.findChildren(QtWidgets.QDialogButtonBox) if box.isVisible()]
|
||
boxes[0].button(QtWidgets.QDialogButtonBox.Ok).click()
|
||
QtTest.QTest.qWait(50)
|
||
assert not Gui.Control.activeDialog()
|
||
assert Gui.activeDocument().getInEdit() is None
|
||
_near(feature.Shape.Volume, 900)
|
||
return {"nativeTaskDialog": True, "nativeOkCancelButtons": True,
|
||
"nativeFeatureEdit": True, "screenshot": str(image_path)}
|
||
case("GUI Control.showDialog and selected profile", task_dialog)
|
||
|
||
def asymmetric_panel():
|
||
doc, body = _doc("ExtrudeAsymmetricPanel")
|
||
sk = _rectangle(body, 10, 6)
|
||
panel = extrude.ExtrusionTaskPanel(doc, sk)
|
||
panel.direction.setCurrentIndex(panel.direction.findData("Asymmetric"))
|
||
panel.length.setValue(4)
|
||
panel.length2.setValue(7)
|
||
panel.update_preview()
|
||
_near(panel._preview.Shape.Volume, 660)
|
||
_near(panel._preview.Shape.BoundBox.ZMin, -7)
|
||
assert panel.length2.isEnabled()
|
||
panel.swap_distances.click()
|
||
panel.update_preview()
|
||
_near(panel._preview.Shape.BoundBox.ZMin, -4)
|
||
_near(panel._preview.Shape.BoundBox.ZMax, 7)
|
||
assert panel.accept(), panel.status.text()
|
||
obj = body.Tip
|
||
_near(obj.Shape.Volume, 660)
|
||
assert str(obj.Direction) == "Асимметрично"
|
||
_near(obj.Length2.Value, 4)
|
||
editor = extrude.ExtrusionTaskPanel(doc, obj.Profile, obj)
|
||
editor.length2.setValue(2)
|
||
editor.update_preview()
|
||
_near(editor._preview.Shape.Volume, 540)
|
||
assert editor.accept(), editor.status.text()
|
||
_near(obj.Shape.Volume, 540)
|
||
doc.undo()
|
||
doc.recompute()
|
||
_near(obj.Shape.Volume, 660)
|
||
assert not any(item.Name.startswith("QI_ExtrudePreview") for item in doc.Objects)
|
||
return {"asymmetricSwapPreviewAcceptEditUndo": True}
|
||
case("GUI asymmetric distances and direction inversion", asymmetric_panel)
|
||
|
||
def native_target_selection():
|
||
import FreeCADGui as Gui
|
||
from PySide import QtCore
|
||
try:
|
||
from PySide6 import QtTest
|
||
except ImportError:
|
||
from PySide2 import QtTest
|
||
_, _, QtWidgets = extrude._gui_modules()
|
||
doc, body = _doc("ExtrudeNativeFaceSelection")
|
||
sk = _rectangle(body, 10, 6)
|
||
target = _plane(doc, 10, 0.5, size=2)
|
||
second = _plane(doc, 14, 0, size=2)
|
||
second.Label = "Вторая конечная плоскость"
|
||
second.Visibility = False
|
||
Gui.Selection.clearSelection()
|
||
Gui.Selection.addSelection(sk)
|
||
panel = extrude.show_dialog()
|
||
panel.extent.setCurrentIndex(panel.extent.findData("To face"))
|
||
assert not panel.length.isEnabled()
|
||
panel.pick_target.click()
|
||
assert panel._picking_target
|
||
Gui.Selection.addSelection(doc.Name, target.Name, "Face1")
|
||
QtTest.QTest.qWait(120)
|
||
assert not panel._picking_target
|
||
assert panel._target_face[0] == target
|
||
assert panel.extend_face.isChecked(), "A small support should automatically extend"
|
||
panel.update_preview()
|
||
expected = panel._preview.Shape.Volume
|
||
_near(expected, 750)
|
||
target.Visibility = False
|
||
Gui.Selection.clearSelection()
|
||
Gui.activeDocument().activeView().viewAxonometric()
|
||
Gui.activeDocument().activeView().fitAll()
|
||
QtTest.QTest.qWait(200)
|
||
assert panel.form.isVisible()
|
||
assert panel.target.text() == "Конечная плоскость · грань 1"
|
||
assert panel.extent.currentText() == "До грани"
|
||
image_path = output / "extrusion-to-oblique-face.png"
|
||
main = Gui.getMainWindow()
|
||
main.grab().save(str(image_path))
|
||
boxes = [box for box in main.findChildren(QtWidgets.QDialogButtonBox) if box.isVisible()]
|
||
assert boxes
|
||
boxes[0].button(QtWidgets.QDialogButtonBox.Ok).click()
|
||
QtTest.QTest.qWait(50)
|
||
assert not Gui.Control.activeDialog()
|
||
obj = body.Tip
|
||
_near(obj.Shape.Volume, expected)
|
||
assert obj.TargetFace[0] == target and obj.ExtendFace
|
||
assert Gui.activeDocument().setEdit(obj.Name)
|
||
QtTest.QTest.qWait(80)
|
||
button = next(item for item in main.findChildren(QtWidgets.QPushButton)
|
||
if item.isVisible() and item.text() == "Выбрать в модели")
|
||
button.click()
|
||
Gui.Selection.addSelection(doc.Name, second.Name, "Face1")
|
||
QtTest.QTest.qWait(120)
|
||
boxes = [box for box in main.findChildren(QtWidgets.QDialogButtonBox) if box.isVisible()]
|
||
boxes[0].button(QtWidgets.QDialogButtonBox.Ok).click()
|
||
QtTest.QTest.qWait(80)
|
||
assert not Gui.Control.activeDialog() and Gui.activeDocument().getInEdit() is None
|
||
_near(obj.Shape.Volume, 840)
|
||
assert obj.TargetFace[0] == second
|
||
doc.undo()
|
||
doc.recompute()
|
||
_near(obj.Shape.Volume, 750)
|
||
assert obj.TargetFace[0] == target
|
||
assert not any(item.Name.startswith("QI_ExtrudePreview") for item in doc.Objects)
|
||
return {"nativeFaceSelectionAndEditUndo": True, "previewEqualsAcceptedSolid": True,
|
||
"screenshot": str(image_path)}
|
||
case("GUI native selection of oblique face, automatic extension and edit undo", native_target_selection)
|
||
|
||
def next_panel():
|
||
import FreeCADGui as Gui
|
||
doc, body, pad = _baseline("ExtrudeNextPanel")
|
||
sk = _rectangle(body, 10, 6, App.Placement(App.Vector(0, 0, 10), App.Rotation()))
|
||
target = doc.addObject("Part::Feature", "NextSolid")
|
||
target.Label = "Тело завершения"
|
||
target.Shape = Part.makeBox(20, 20, 5, App.Vector(-1, -1, 20))
|
||
doc.recompute()
|
||
panel = extrude.ExtrusionTaskPanel(doc, sk)
|
||
panel.extent.setCurrentIndex(panel.extent.findData("To next"))
|
||
panel.pick_target.click()
|
||
Gui.Selection.addSelection(target)
|
||
panel.update_preview()
|
||
expected = panel._preview.Shape.Volume
|
||
_near(expected, 4600)
|
||
assert panel.target.text() == target.Label
|
||
assert panel.accept(), panel.status.text()
|
||
obj = body.Tip
|
||
assert obj.Terminator == target
|
||
_near(obj.Shape.Volume, expected)
|
||
doc.undo()
|
||
doc.recompute()
|
||
assert body.Tip == pad
|
||
assert not any(item.Name.startswith("QI_ExtrudePreview") for item in doc.Objects)
|
||
return {"nextTerminatorSelectionPreviewAcceptUndo": True}
|
||
case("GUI selected next-face terminator", next_panel)
|
||
|
||
def target_error_rollback():
|
||
doc, body = _doc("ExtrudeInvalidFacePanel")
|
||
sk = _rectangle(body, 10, 6)
|
||
crossing = _plane(doc, -2, 0.5)
|
||
original = set(item.Name for item in doc.Objects)
|
||
panel = extrude.ExtrusionTaskPanel(doc, sk)
|
||
panel.extent.setCurrentIndex(panel.extent.findData("To face"))
|
||
panel.set_target(crossing, "Face1")
|
||
panel.update_preview()
|
||
assert "пересекает профиль" in panel.status.text()
|
||
assert not panel.accept()
|
||
assert original == set(item.Name for item in doc.Objects)
|
||
assert panel.reject()
|
||
assert original == set(item.Name for item in doc.Objects)
|
||
return {"invalidTargetAcceptRollbackAndCancel": True}
|
||
case("GUI invalid target rolls back and cancels cleanly", target_error_rollback)
|
||
|
||
def native_operation_edit():
|
||
import FreeCADGui as Gui
|
||
try:
|
||
from PySide6 import QtTest
|
||
except ImportError:
|
||
from PySide2 import QtTest
|
||
_, _, QtWidgets = extrude._gui_modules()
|
||
doc, body, pad = _baseline("ExtrudeNativeOperationEdit")
|
||
sk, _ = _circle(body, 3, z=10, x=10, y=10)
|
||
obj = extrude.create_extrusion(sk, 3)
|
||
assert Gui.activeDocument().setEdit(obj.Name)
|
||
QtTest.QTest.qWait(50)
|
||
main = Gui.getMainWindow()
|
||
operation = main.findChild(QtWidgets.QComboBox, "QI_ExtrudeOperation")
|
||
operation.setCurrentIndex(operation.findData("New Solid"))
|
||
QtTest.QTest.qWait(120)
|
||
boxes = [box for box in main.findChildren(QtWidgets.QDialogButtonBox) if box.isVisible()]
|
||
assert boxes
|
||
boxes[0].button(QtWidgets.QDialogButtonBox.Ok).click()
|
||
QtTest.QTest.qWait(80)
|
||
assert not Gui.Control.activeDialog() and Gui.activeDocument().getInEdit() is None
|
||
assert body.Tip == pad and extrude._body_of(obj) != body
|
||
_near(obj.Shape.Volume, math.pi * 27)
|
||
assert Gui.activeDocument().setEdit(obj.Name)
|
||
QtTest.QTest.qWait(50)
|
||
operation = main.findChild(QtWidgets.QComboBox, "QI_ExtrudeOperation")
|
||
direction = main.findChild(QtWidgets.QComboBox, "QI_ExtrudeDirection")
|
||
operation.setCurrentIndex(operation.findData("Cut"))
|
||
direction.setCurrentIndex(direction.findData("Reverse"))
|
||
QtTest.QTest.qWait(120)
|
||
boxes = [box for box in main.findChildren(QtWidgets.QDialogButtonBox) if box.isVisible()]
|
||
boxes[0].button(QtWidgets.QDialogButtonBox.Ok).click()
|
||
QtTest.QTest.qWait(80)
|
||
assert not Gui.Control.activeDialog() and Gui.activeDocument().getInEdit() is None
|
||
assert body.Tip == obj and extrude._body_of(obj) == body
|
||
_near(obj.Shape.Volume, 4000 - math.pi * 27)
|
||
doc.undo()
|
||
doc.recompute()
|
||
assert body.Tip == pad and extrude._body_of(obj) != body
|
||
assert not any(item.Name.startswith("QI_ExtrudePreview") for item in doc.Objects)
|
||
return {"nativeOperationChangeAndBodyMigrationUndo": True}
|
||
case("GUI native operation edits migrate solid history and undo", native_operation_edit)
|
||
|
||
for name in documents:
|
||
if name in App.listDocuments():
|
||
App.closeDocument(name)
|
||
report = {"passed": all(item["passed"] for item in results),
|
||
"count": len(results), "gui": bool(App.GuiUp), "scenarios": results}
|
||
(output / "extrude-suite.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
return report
|
||
|
||
|
||
def _autorun():
|
||
report = run(os.environ.get("QI_EXTRUDE_OUTPUT", str(SOURCE.parents[1] / "artifacts" / "extrude")))
|
||
print(json.dumps(report, ensure_ascii=False))
|
||
if App.GuiUp and os.environ.get("QI_EXTRUDE_GUI_EXIT") == "1":
|
||
from PySide import QtCore
|
||
QtCore.QTimer.singleShot(200, QtCore.QCoreApplication.quit)
|
||
if not report["passed"]:
|
||
raise RuntimeError("Extrusion regression suite failed")
|
||
|
||
|
||
if os.environ.get("QI_RUN_EXTRUDE_SUITE") == "1":
|
||
if App.GuiUp:
|
||
# Startup macros run before the native Task dock has been installed.
|
||
# Let the real main window finish startup before testing showDialog.
|
||
from PySide import QtCore
|
||
QtCore.QTimer.singleShot(900, _autorun)
|
||
else:
|
||
_autorun()
|