531 lines
25 KiB
Python
531 lines
25 KiB
Python
"""Real-kernel regression checks for associated sketch projections, no GUI."""
|
|
import json
|
|
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_projection as projection
|
|
import qi_extrude as extrude
|
|
|
|
|
|
def _near(actual, expected, tolerance=1e-7):
|
|
if abs(actual - expected) > tolerance * max(1., abs(expected)):
|
|
raise AssertionError("Expected %.12g, obtained %.12g" % (expected, actual))
|
|
|
|
|
|
def _valid(obj):
|
|
assert not any(str(flag).lower().startswith(("invalid", "error")) for flag in obj.State), obj.State
|
|
|
|
|
|
def _base(name, body=False, plane="XY"):
|
|
doc = App.newDocument(name)
|
|
doc.Label = "Проверка проекции"
|
|
doc.UndoMode = 1
|
|
if body:
|
|
group = doc.addObject("PartDesign::Body", "Body")
|
|
source = group.newObject("PartDesign::AdditiveBox", "Base")
|
|
sketch = group.newObject("Sketcher::SketchObject", "Sketch")
|
|
else:
|
|
group = None
|
|
source = doc.addObject("Part::Box", "Base")
|
|
sketch = doc.addObject("Sketcher::SketchObject", "Sketch")
|
|
source.Label = "Основание"
|
|
source.Length, source.Width, source.Height = 20, 10, 8
|
|
sketch.Label = "Эскиз"
|
|
if plane == "XZ":
|
|
sketch.Placement.Rotation = App.Rotation(App.Vector(1, 0, 0), 90)
|
|
elif plane == "YZ":
|
|
sketch.Placement.Rotation = App.Rotation(App.Vector(1, 1, 1), 120)
|
|
doc.recompute()
|
|
_valid(source)
|
|
return doc, source, sketch, group
|
|
|
|
|
|
def _top(source):
|
|
return next("Face%d" % (i + 1) for i, face in enumerate(source.Shape.Faces)
|
|
if face.normalAt(0, 0).z > .99)
|
|
|
|
|
|
def run(output_dir):
|
|
output = Path(output_dir)
|
|
output.mkdir(parents=True, exist_ok=True)
|
|
checks, documents = [], []
|
|
|
|
def check(name, function):
|
|
try:
|
|
evidence = function()
|
|
checks.append({"name": name, "passed": True, "evidence": evidence})
|
|
except Exception:
|
|
checks.append({"name": name, "passed": False, "error": traceback.format_exc()})
|
|
|
|
def base(name, **kwargs):
|
|
values = _base(name, **kwargs)
|
|
documents.append(values[0].Name)
|
|
return values
|
|
|
|
def defining_and_update():
|
|
doc, source, sketch, _ = base("ProjectionDefining")
|
|
ids = projection.project_geometry(sketch, source, [_top(source)])
|
|
assert len(ids) == 4 and len(sketch.ExternalGeometry) == 1
|
|
assert len(sketch.Shape.Wires) == 1 and sketch.Shape.Wires[0].isClosed()
|
|
_near(extrude._profile_face(sketch).Area, 200)
|
|
source.Length = 30
|
|
doc.recompute()
|
|
_near(sketch.Shape.Length, 80)
|
|
_near(extrude._profile_face(sketch).Area, 300)
|
|
_valid(sketch)
|
|
return {"native_ids": ids, "source_length_mm": source.Length.Value,
|
|
"updated_profile_area_mm2": 300}
|
|
|
|
def single_edge_and_vertex():
|
|
doc, source, sketch, _ = base("ProjectionElements")
|
|
edge = projection.project_geometry(sketch, source, "Edge1", construction=True)
|
|
vertex = projection.project_geometry(sketch, source, "Vertex1", construction=True)
|
|
assert edge and vertex and len(projection._links(sketch)) == 2
|
|
assert sketch.Shape.isNull()
|
|
_valid(sketch)
|
|
return {"edge_ids": edge, "vertex_ids": vertex, "profile_excludes_references": True}
|
|
|
|
def construction_exclusion():
|
|
doc, source, sketch, _ = base("ProjectionConstruction")
|
|
ids = projection.project_geometry(sketch, source, _top(source), construction=True)
|
|
assert len(ids) == 4 and sketch.Shape.isNull()
|
|
try:
|
|
extrude._profile_face(sketch)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
raise AssertionError("Construction-only projection must not be extrudable")
|
|
source.Length = 30
|
|
doc.recompute()
|
|
perimeter = sum(g.toShape().Length for g in sketch.ExternalGeo[2:])
|
|
_near(perimeter, 80)
|
|
return {"reference_perimeter_mm": perimeter, "extrusion_rejected": True}
|
|
|
|
def attachment_extrusion():
|
|
doc, source, sketch, body = base("ProjectionAttached", body=True)
|
|
top = _top(source)
|
|
sketch.AttachmentSupport = (source, [top])
|
|
sketch.MapMode = "FlatFace"
|
|
doc.recompute()
|
|
projection.project_geometry(sketch, source, top)
|
|
feature = extrude.create_extrusion(sketch, 3, operation="Join")
|
|
_near(feature.Shape.Volume, 2200)
|
|
source.Length = 30
|
|
doc.recompute()
|
|
_near(feature.Shape.Volume, 3300)
|
|
_valid(feature)
|
|
_valid(body)
|
|
return {"map_mode": sketch.MapMode, "updated_join_volume_mm3": feature.Shape.Volume}
|
|
|
|
def body_selection_captures_tip():
|
|
doc, source, sketch, body = base("ProjectionBodySource", body=True)
|
|
projection.project_geometry(sketch, body, _top(source))
|
|
assert sketch.ExternalGeometry[0][0] == source
|
|
assert body not in sketch.OutList
|
|
return {"source_type": source.TypeId, "body_reference_cycle_avoided": True}
|
|
|
|
def rotated_planes():
|
|
volumes = {}
|
|
for plane, face_dimension in [("XZ", 160), ("YZ", 80)]:
|
|
doc, source, sketch, body = base("Projection" + plane, body=True, plane=plane)
|
|
wanted = App.Vector(0, -1, 0) if plane == "XZ" else App.Vector(1, 0, 0)
|
|
face = next("Face%d" % (i + 1) for i, f in enumerate(source.Shape.Faces)
|
|
if f.normalAt(0, 0).dot(wanted) > .99)
|
|
projection.project_geometry(sketch, source, face)
|
|
_near(extrude._profile_face(sketch).Area, face_dimension)
|
|
feature = extrude.create_extrusion(sketch, 2, operation="New Solid")
|
|
_near(feature.Shape.Volume, face_dimension * 2)
|
|
_valid(feature)
|
|
volumes[plane] = feature.Shape.Volume
|
|
return {"projection_extrusion_volumes_mm3": volumes}
|
|
|
|
def all_loops_and_holes():
|
|
doc = App.newDocument("ProjectionHole")
|
|
documents.append(doc.Name)
|
|
doc.UndoMode = 1
|
|
body = doc.addObject("PartDesign::Body", "Body")
|
|
source = body.newObject("PartDesign::Feature", "Base")
|
|
source.Shape = Part.makeBox(20, 10, 8).cut(Part.makeCylinder(2, 8, App.Vector(10, 5, 0)))
|
|
sketch = body.newObject("Sketcher::SketchObject", "Sketch")
|
|
doc.recompute()
|
|
top = _top(source)
|
|
sketch.AttachmentSupport = (source, [top])
|
|
sketch.MapMode = "FlatFace"
|
|
doc.recompute()
|
|
ids = projection.project_geometry(sketch, source, top)
|
|
assert len(ids) == 5 and len(sketch.Shape.Wires) == 2
|
|
face = extrude._profile_face(sketch)
|
|
_near(face.Area, 200 - 4 * 3.141592653589793)
|
|
feature = extrude.create_extrusion(sketch, 2, operation="Join")
|
|
_near(feature.Shape.Volume, face.Area * 10)
|
|
_valid(feature)
|
|
return {"projected_edges": len(ids), "wires": 2, "hole_preserved_volume_mm3": feature.Shape.Volume}
|
|
|
|
def part_placement():
|
|
doc, source, sketch, body = base("ProjectionPartPlacement", body=True)
|
|
part = doc.addObject("App::Part", "Component")
|
|
part.addObject(body)
|
|
part.Placement = App.Placement(App.Vector(40, -25, 18), App.Rotation(App.Vector(1, 1, 1), 57))
|
|
body.Placement = App.Placement(App.Vector(3, 4, 6), App.Rotation(App.Vector(0, 0, 1), 23))
|
|
top = _top(source)
|
|
sketch.AttachmentSupport = (source, [top])
|
|
sketch.MapMode = "FlatFace"
|
|
doc.recompute()
|
|
projection.project_geometry(sketch, source, top)
|
|
_near(extrude._profile_face(sketch).Area, 200)
|
|
feature = extrude.create_extrusion(sketch, 3, operation="Join")
|
|
_near(feature.Shape.Volume, 2200)
|
|
source.Length = 30
|
|
doc.recompute()
|
|
_near(feature.Shape.Volume, 3300)
|
|
_valid(body)
|
|
_valid(feature)
|
|
return {"placed_part_and_body": True, "parametric_world_placement_volume_mm3": feature.Shape.Volume}
|
|
|
|
def circle_and_arc_unlink():
|
|
doc = App.newDocument("ProjectionCurves")
|
|
documents.append(doc.Name)
|
|
doc.UndoMode = 1
|
|
cylinder = doc.addObject("Part::Cylinder", "Cylinder")
|
|
cylinder.Radius, cylinder.Height = 4, 8
|
|
circle_sketch = doc.addObject("Sketcher::SketchObject", "CircleSketch")
|
|
doc.recompute()
|
|
projection.project_geometry(circle_sketch, cylinder, _top(cylinder))
|
|
_near(extrude._profile_face(circle_sketch).Area, 16 * 3.141592653589793)
|
|
projection.break_links(circle_sketch)
|
|
assert circle_sketch.Geometry[0].isDerivedFrom("Part::GeomCircle")
|
|
cylinder.Radius = 6
|
|
doc.recompute()
|
|
_near(extrude._profile_face(circle_sketch).Area, 16 * 3.141592653589793)
|
|
arc_source = doc.addObject("Part::Feature", "Arc")
|
|
arc_source.Shape = Part.Arc(App.Vector(0, 0, 0), App.Vector(5, 5, 0), App.Vector(10, 0, 0)).toShape()
|
|
arc_sketch = doc.addObject("Sketcher::SketchObject", "ArcSketch")
|
|
doc.recompute()
|
|
projection.project_geometry(arc_sketch, arc_source, "Edge1")
|
|
before = arc_sketch.Shape.Length
|
|
projection.break_links(arc_sketch)
|
|
assert arc_sketch.Geometry[0].isDerivedFrom("Part::GeomArcOfCircle")
|
|
_near(arc_sketch.Shape.Length, before)
|
|
_valid(arc_sketch)
|
|
return {"circle_radius_preserved_mm": 4, "arc_length_preserved_mm": before}
|
|
|
|
def reference_dimension_unlink():
|
|
doc, source, sketch, _ = base("ProjectionReferenceDimension")
|
|
ids = projection.project_geometry(sketch, source, _top(source), construction=True)
|
|
geometry = sketch.ExternalGeo[-ids[0] - 1]
|
|
dimension = Sketcher.Constraint("Distance", ids[0], geometry.toShape().Length)
|
|
index = sketch.addConstraint(dimension)
|
|
sketch.setDriving(index, False)
|
|
sketch.renameConstraint(index, "ReferenceLength")
|
|
doc.recompute()
|
|
before = sketch.Constraints[index].Value
|
|
mapping = projection.break_links(sketch, [ids[0]])
|
|
assert sketch.Constraints[index].First == mapping[ids[0]]
|
|
assert not sketch.Constraints[index].Driving
|
|
assert sketch.Constraints[index].Name == "ReferenceLength"
|
|
_near(sketch.Constraints[index].Value, before)
|
|
_valid(sketch)
|
|
return {"reference_dimension_mm": before, "name_and_reference_mode_retained": True}
|
|
|
|
def unlink_preserves_geometry():
|
|
doc, source, sketch, _ = base("ProjectionUnlink")
|
|
ids = projection.project_geometry(sketch, source, _top(source))
|
|
before = sketch.Shape.copy()
|
|
mapping = projection.break_links(sketch, [ids[0]])
|
|
assert len(mapping) == 4 and not sketch.ExternalGeometry and sketch.GeometryCount == 4
|
|
_near(sketch.Shape.Length, before.Length)
|
|
_near(extrude._profile_face(sketch).Area, 200)
|
|
source.Length = 40
|
|
doc.recompute()
|
|
_near(extrude._profile_face(sketch).Area, 200)
|
|
_valid(sketch)
|
|
return {"old_to_independent_ids": mapping, "independent_area_mm2": 200}
|
|
|
|
def unlink_construction():
|
|
doc, source, sketch, _ = base("ProjectionUnlinkConstruction")
|
|
projection.project_geometry(sketch, source, _top(source), construction=True)
|
|
projection.break_links(sketch)
|
|
assert sketch.Shape.isNull() and all(sketch.getConstruction(i) for i in range(4))
|
|
assert not sketch.ExternalGeometry
|
|
return {"independent_construction_curves": 4, "profile_empty": True}
|
|
|
|
def grouped_source_links():
|
|
doc, source, sketch, _ = base("ProjectionGroupedSource")
|
|
face_ids = projection.project_geometry(sketch, source, _top(source))
|
|
edge_ids = projection.project_geometry(sketch, source, "Edge2", construction=True)
|
|
assert len(projection._links(sketch)) == 2
|
|
projection.break_links(sketch, [face_ids[0]])
|
|
assert len(projection._links(sketch)) == 1
|
|
assert projection._links(sketch)[0] == (source, "Edge2")
|
|
assert len(projection.linked_geometry_ids(sketch)) == len(edge_ids)
|
|
_near(extrude._profile_face(sketch).Area, 200)
|
|
source.Length = 35
|
|
doc.recompute()
|
|
_near(extrude._profile_face(sketch).Area, 200)
|
|
_valid(sketch)
|
|
return {"same_source_independent_face_and_retained_edge_link": True}
|
|
|
|
def vertex_selection_and_unlink():
|
|
from types import SimpleNamespace
|
|
doc, source, sketch, _ = base("ProjectionVertexUnlink")
|
|
ids = projection.project_geometry(sketch, source, "Vertex1", construction=True)
|
|
linked_file = output / "vertex-projection-linked.FCStd"
|
|
doc.saveAs(str(linked_file))
|
|
App.closeDocument(doc.Name)
|
|
doc = App.openDocument(str(linked_file))
|
|
documents.append(doc.Name)
|
|
doc.UndoMode = 1
|
|
source, sketch = doc.Base, doc.Sketch
|
|
assert projection.linked_geometry_ids(sketch) == ids
|
|
vertex_number = next(i + 1 for i in range(100)
|
|
if sketch.getGeoVertexIndex(i)[0] == ids[0])
|
|
selection = SimpleNamespace(Object=sketch, SubElementNames=["Vertex%d" % vertex_number])
|
|
assert projection.selected_external_ids(sketch, [selection]) == ids
|
|
point = sketch.ExternalGeo[-ids[0] - 1].toShape().Vertexes[0].Point
|
|
line = sketch.addGeometry(Part.LineSegment(point, point + App.Vector(3, 4, 0)), False)
|
|
constraint = sketch.addConstraint(Sketcher.Constraint("Coincident", line, 1, ids[0], 1))
|
|
sketch.renameConstraint(constraint, "AtProjectedPoint")
|
|
doc.recompute()
|
|
mapping = projection.break_links(sketch, ids)
|
|
assert len(mapping) == 1 and sketch.getConstruction(mapping[ids[0]])
|
|
actual = sketch.Geometry[mapping[ids[0]]].toShape().Vertexes[0].Point
|
|
_near((point - actual).Length, 0)
|
|
assert not sketch.ExternalGeometry
|
|
assert sketch.Constraints[constraint].Second == mapping[ids[0]]
|
|
doc.undo()
|
|
doc.recompute()
|
|
assert projection.linked_geometry_ids(sketch) == ids
|
|
assert sketch.Constraints[constraint].Second == ids[0]
|
|
doc.redo()
|
|
doc.recompute()
|
|
assert not sketch.ExternalGeometry
|
|
assert sketch.Constraints[constraint].Second == mapping[ids[0]]
|
|
independent_file = output / "vertex-projection-independent.FCStd"
|
|
doc.saveAs(str(independent_file))
|
|
App.closeDocument(doc.Name)
|
|
restored = App.openDocument(str(independent_file))
|
|
documents.append(restored.Name)
|
|
restored.Base.Placement.Base = App.Vector(15, 0, 0)
|
|
restored.recompute()
|
|
assert not projection.linked_geometry_ids(restored.Sketch)
|
|
assert restored.Sketch.Constraints[constraint].Second == mapping[ids[0]]
|
|
assert restored.Sketch.Constraints[constraint].Name == "AtProjectedPoint"
|
|
_near((restored.Sketch.Geometry[line].StartPoint - point).Length, 0)
|
|
_valid(restored.Sketch)
|
|
return {"vertex_selection_name": selection.SubElementNames[0], "independent_point_preserved": True,
|
|
"point_constraints_undo_redo_save_reopen": True}
|
|
|
|
def unlink_constraints_and_remaining_links():
|
|
doc, source, sketch, _ = base("ProjectionUnlinkConstraints")
|
|
ids = projection.project_geometry(sketch, source, _top(source), construction=True)
|
|
other = doc.addObject("Part::Box", "Other")
|
|
other.Placement.Base = App.Vector(30, 0, 0)
|
|
doc.recompute()
|
|
remaining = projection.project_geometry(sketch, other, "Edge2", construction=True)
|
|
endpoint = sketch.ExternalGeo[-ids[0] - 1].StartPoint
|
|
line = sketch.addGeometry(Part.LineSegment(endpoint, endpoint + App.Vector(3, 4, 0)), False)
|
|
sketch.addConstraint(Sketcher.Constraint("Coincident", line, 1, ids[0], 1))
|
|
length = sketch.addConstraint(Sketcher.Constraint("Distance", line, 5.))
|
|
sketch.renameConstraint(length, "LineLength")
|
|
sketch.setExpression("Constraints.LineLength", "Base.Width / 2")
|
|
before_expressions = dict(sketch.ExpressionEngine)
|
|
second = sketch.addGeometry(Part.LineSegment(App.Vector(0, 0, 0), App.Vector(0, -3, 0)), True)
|
|
sketch.addConstraint(Sketcher.Constraint("PointOnObject", second, 1, remaining[0]))
|
|
doc.recompute()
|
|
_valid(sketch)
|
|
before_constraints = len(sketch.Constraints)
|
|
before_start = sketch.Geometry[line].StartPoint
|
|
mapping = projection.break_links(sketch, [ids[0]])
|
|
assert len(sketch.Constraints) == before_constraints
|
|
assert len(sketch.ExternalGeometry) == 1 and sketch.ExternalGeometry[0][0] == other
|
|
assert dict(sketch.ExpressionEngine) == before_expressions
|
|
assert sketch.Constraints[0].Second == mapping[ids[0]]
|
|
assert sketch.Constraints[-1].Second == -3
|
|
source.Length = 40
|
|
doc.recompute()
|
|
_near((sketch.Geometry[line].StartPoint - before_start).Length, 0)
|
|
_valid(sketch)
|
|
path = output / "projection-constraints.FCStd"
|
|
doc.saveAs(str(path))
|
|
App.closeDocument(doc.Name)
|
|
restored = App.openDocument(str(path))
|
|
documents.append(restored.Name)
|
|
assert dict(restored.Sketch.ExpressionEngine) == before_expressions
|
|
assert restored.Sketch.Constraints[0].Second == mapping[ids[0]]
|
|
assert restored.Sketch.Constraints[-1].Second == -3
|
|
assert len(restored.Sketch.Constraints) == before_constraints
|
|
_valid(restored.Sketch)
|
|
return {"constraints_retained": before_constraints, "expression": "Base.Width / 2",
|
|
"remaining_external_constraint_remapped": -3, "save_reopen_preserved": True}
|
|
|
|
def undo_redo():
|
|
doc, source, sketch, _ = base("ProjectionUndo")
|
|
projection.project_geometry(sketch, source, _top(source))
|
|
doc.undo()
|
|
doc.recompute()
|
|
assert not sketch.ExternalGeometry and sketch.Shape.isNull()
|
|
doc.redo()
|
|
doc.recompute()
|
|
_near(sketch.Shape.Length, 60)
|
|
projection.break_links(sketch)
|
|
doc.undo()
|
|
doc.recompute()
|
|
assert len(sketch.ExternalGeometry) == 1 and sketch.GeometryCount == 0
|
|
doc.redo()
|
|
doc.recompute()
|
|
assert not sketch.ExternalGeometry and sketch.GeometryCount == 4
|
|
_near(sketch.Shape.Length, 60)
|
|
return {"projection_and_unlink_undo_redo": True}
|
|
|
|
def save_reopen():
|
|
doc, source, sketch, _ = base("ProjectionPersistence")
|
|
projection.project_geometry(sketch, source, _top(source))
|
|
path = output / "associated-projection.FCStd"
|
|
doc.saveAs(str(path))
|
|
App.closeDocument(doc.Name)
|
|
reopened = App.openDocument(str(path))
|
|
documents.append(reopened.Name)
|
|
reopened.Base.Length = 30
|
|
reopened.recompute()
|
|
_near(extrude._profile_face(reopened.Sketch).Area, 300)
|
|
projection.break_links(reopened.Sketch)
|
|
independent = output / "independent-projection.FCStd"
|
|
reopened.saveAs(str(independent))
|
|
App.closeDocument(reopened.Name)
|
|
final = App.openDocument(str(independent))
|
|
documents.append(final.Name)
|
|
final.Base.Length = 40
|
|
final.recompute()
|
|
_near(extrude._profile_face(final.Sketch).Area, 300)
|
|
assert not final.Sketch.ExternalGeometry
|
|
return {"associated_file": str(path), "independent_file": str(independent),
|
|
"independent_area_after_reload_mm2": 300}
|
|
|
|
def rejects_atomically():
|
|
doc, source, sketch, _ = base("ProjectionRejects")
|
|
cases = [(["Edge1", "Edge9999"], None), (["Face6", "Face6"], None),
|
|
(["Solid1"], None), ([], None)]
|
|
rejected = 0
|
|
for names, _ in cases:
|
|
before = sketch.dumpContent()
|
|
try:
|
|
projection.project_geometry(sketch, source, names)
|
|
except ValueError:
|
|
rejected += 1
|
|
else:
|
|
raise AssertionError("Invalid projection selection accepted: %s" % names)
|
|
assert sketch.dumpContent() == before
|
|
projection.project_geometry(sketch, source, _top(source))
|
|
before = sketch.dumpContent()
|
|
for selection in [[-3, 0], [-999], []]:
|
|
try:
|
|
projection.break_links(sketch, selection)
|
|
except ValueError:
|
|
rejected += 1
|
|
else:
|
|
raise AssertionError("Invalid unlink selection accepted")
|
|
assert sketch.dumpContent() == before
|
|
return {"invalid_batches_rejected_without_mutation": rejected}
|
|
|
|
def cyclic_and_cross_document():
|
|
doc, source, sketch, _ = base("ProjectionCycle")
|
|
dependent = doc.addObject("Part::Feature", "Dependent")
|
|
dependent.addProperty("App::PropertyLink", "Profile")
|
|
dependent.Profile = sketch
|
|
dependent.Shape = source.Shape.copy()
|
|
second, foreign, _, _ = base("ProjectionForeign")
|
|
count = 0
|
|
for obj in [dependent, foreign, sketch]:
|
|
before = sketch.dumpContent()
|
|
try:
|
|
projection.project_geometry(sketch, obj, "Edge1")
|
|
except ValueError:
|
|
count += 1
|
|
else:
|
|
raise AssertionError("Cyclic or cross-document reference accepted")
|
|
assert sketch.dumpContent() == before
|
|
return {"invalid_reference_sources_rejected": count}
|
|
|
|
def solver_failure_rolls_back():
|
|
doc, source, sketch, _ = base("ProjectionSolverRollback")
|
|
line = sketch.addGeometry(Part.LineSegment(App.Vector(1, 2, 0), App.Vector(6, 2, 0)), False)
|
|
sketch.addConstraint(Sketcher.Constraint("Distance", line, 5.))
|
|
sketch.addConstraint(Sketcher.Constraint("Distance", line, 7.))
|
|
doc.recompute()
|
|
before_constraints = [(c.Type, c.First, c.FirstPos, c.Second, c.SecondPos, c.Value)
|
|
for c in sketch.Constraints]
|
|
before_start, before_end = sketch.Geometry[0].StartPoint, sketch.Geometry[0].EndPoint
|
|
try:
|
|
projection.project_geometry(sketch, source, _top(source))
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
raise AssertionError("Conflicting sketch must reject projection")
|
|
assert not sketch.ExternalGeometry and sketch.GeometryCount == 1
|
|
assert [(c.Type, c.First, c.FirstPos, c.Second, c.SecondPos, c.Value)
|
|
for c in sketch.Constraints] == before_constraints
|
|
_near((sketch.Geometry[0].StartPoint - before_start).Length, 0)
|
|
_near((sketch.Geometry[0].EndPoint - before_end).Length, 0)
|
|
return {"failed_native_solver_transaction_rolled_back": True}
|
|
|
|
def selection_mapping():
|
|
from types import SimpleNamespace
|
|
doc, source, sketch, _ = base("ProjectionSelectionMapping")
|
|
ids = projection.project_geometry(sketch, source, _top(source))
|
|
item = SimpleNamespace(Object=sketch, SubElementNames=["ExternalEdge1", "ExternalEdge4"])
|
|
assert projection.selected_external_ids(sketch, [item]) == [ids[0], ids[3]]
|
|
try:
|
|
projection.selected_external_ids(sketch, [SimpleNamespace(Object=sketch, SubElementNames=["Edge1"])])
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
raise AssertionError("Internal edge must not be treated as external")
|
|
return {"ExternalEdge1": -3, "ExternalEdge4": -6}
|
|
|
|
for name, function in [
|
|
("Defining projection is linked and regenerates", defining_and_update),
|
|
("Single edge and vertex projections", single_edge_and_vertex),
|
|
("Construction references regenerate and are excluded from extrusion", construction_exclusion),
|
|
("Face-supported sketch and parametric extrusion", attachment_extrusion),
|
|
("Body selection captures preceding solid feature", body_selection_captures_tip),
|
|
("XZ and YZ world placement", rotated_planes),
|
|
("Face projection includes inner hole boundaries", all_loops_and_holes),
|
|
("Face attachment in rotated Part and Body preserves world placement", part_placement),
|
|
("Break Link retains native circles and arcs", circle_and_arc_unlink),
|
|
("Break Link preserves native reference dimensions", reference_dimension_unlink),
|
|
("Break Link retains geometry and stops dependency", unlink_preserves_geometry),
|
|
("Break Link preserves construction state", unlink_construction),
|
|
("Selective unlink preserves grouped links to the same source", grouped_source_links),
|
|
("Native vertex selection and independent point unlink", vertex_selection_and_unlink),
|
|
("Break Link preserves constraints expressions and other references", unlink_constraints_and_remaining_links),
|
|
("Projection and Break Link native undo redo", undo_redo),
|
|
("Associated and independent geometry save reopen", save_reopen),
|
|
("Invalid mixed batches reject atomically", rejects_atomically),
|
|
("Cyclic and cross-document sources reject", cyclic_and_cross_document),
|
|
("Native solver failure aborts projection transaction", solver_failure_rolls_back),
|
|
("Native external selection names map to GeoIds", selection_mapping),
|
|
]:
|
|
check(name, function)
|
|
|
|
for name in set(documents):
|
|
if name in App.listDocuments():
|
|
App.closeDocument(name)
|
|
result = {"passed": all(c["passed"] for c in checks), "checks": checks,
|
|
"summary": {"total": len(checks), "passed": sum(c["passed"] for c in checks),
|
|
"failed": sum(not c["passed"] for c in checks)}, "gui_exercised": False}
|
|
(output / "projection-suite.json").write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
return result
|
|
|
|
|
|
if __name__ == "__main__":
|
|
result = run(sys.argv[1] if len(sys.argv) > 1 else Path(__file__).resolve().parents[1] / "artifacts" / "projection")
|
|
print(json.dumps(result["summary"]))
|