# **************************************************************************
#   Copyright (c) 2011 Juergen Riegel <FreeCAD@juergen-riegel.net>        *
#                                                                         *
#   This file is part of the FreeCAD CAx development system.              *
#                                                                         *
#   This program is free software; you can redistribute it and/or modify  *
#   it under the terms of the GNU Lesser General Public License (LGPL)    *
#   as published by the Free Software Foundation; either version 2 of     *
#   the License, or (at your option) any later version.                   *
#   for detail see the LICENCE text file.                                 *
#                                                                         *
#   FreeCAD is distributed in the hope that it will be useful,            *
#   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
#   GNU Library General Public License for more details.                  *
#                                                                         *
#   You should have received a copy of the GNU Library General Public     *
#   License along with FreeCAD; if not, write to the Free Software        *
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
#   USA                                                                   *
# **************************************************************************


import FreeCAD, os, sys, unittest, Part, Sketcher
from Part import Precision

App = FreeCAD

xy_normal = FreeCAD.Vector(0, 0, 1)


def vec(x, y):
    """Shorthand to create a vector in the XY-plane"""
    return FreeCAD.Vector(x, y, 0)


def CreateRectangleSketch(SketchFeature, corner, lengths):
    hmin, hmax = corner[0], corner[0] + lengths[0]
    vmin, vmax = corner[1], corner[1] + lengths[1]

    # add the geometry and grab the count offset
    i = int(SketchFeature.GeometryCount)
    SketchFeature.addGeometry(
        Part.LineSegment(FreeCAD.Vector(hmin, vmax), FreeCAD.Vector(hmax, vmax, 0))
    )
    SketchFeature.addGeometry(
        Part.LineSegment(FreeCAD.Vector(hmax, vmax, 0), FreeCAD.Vector(hmax, vmin, 0))
    )
    SketchFeature.addGeometry(
        Part.LineSegment(FreeCAD.Vector(hmax, vmin, 0), FreeCAD.Vector(hmin, vmin, 0))
    )
    SketchFeature.addGeometry(
        Part.LineSegment(FreeCAD.Vector(hmin, vmin, 0), FreeCAD.Vector(hmin, vmax, 0))
    )

    # add the rectangular constraints
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", i + 0, 2, i + 1, 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", i + 1, 2, i + 2, 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", i + 2, 2, i + 3, 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", i + 3, 2, i + 0, 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Horizontal", i + 0))
    SketchFeature.addConstraint(Sketcher.Constraint("Horizontal", i + 2))
    SketchFeature.addConstraint(Sketcher.Constraint("Vertical", i + 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Vertical", i + 3))

    # Fix the bottom left corner of the rectangle
    SketchFeature.addConstraint(Sketcher.Constraint("DistanceX", i + 2, 2, corner[0]))
    SketchFeature.addConstraint(Sketcher.Constraint("DistanceY", i + 2, 2, corner[1]))

    # add dimensions
    if lengths[0] == lengths[1]:
        SketchFeature.addConstraint(Sketcher.Constraint("Equal", i + 2, i + 3))
        SketchFeature.addConstraint(Sketcher.Constraint("Distance", i + 0, hmax - hmin))
    else:
        SketchFeature.addConstraint(Sketcher.Constraint("Distance", i + 1, vmax - vmin))
        SketchFeature.addConstraint(Sketcher.Constraint("Distance", i + 0, hmax - hmin))


def CreateCircleSketch(SketchFeature, center, radius):
    i = int(SketchFeature.GeometryCount)
    SketchFeature.addGeometry(Part.Circle(App.Vector(*center), App.Vector(0, 0, 1), radius), False)
    SketchFeature.addConstraint(Sketcher.Constraint("Radius", i, radius))
    SketchFeature.addConstraint(Sketcher.Constraint("DistanceX", i, 3, center[0]))
    SketchFeature.addConstraint(Sketcher.Constraint("DistanceY", i, 3, center[1]))


def CreateBoxSketchSet(SketchFeature):
    SketchFeature.addGeometry(
        Part.LineSegment(
            FreeCAD.Vector(-99.230339, 36.960674, 0),
            FreeCAD.Vector(69.432587, 36.960674, 0),
        )
    )
    SketchFeature.addGeometry(
        Part.LineSegment(
            FreeCAD.Vector(69.432587, 36.960674, 0),
            FreeCAD.Vector(69.432587, -53.196629, 0),
        )
    )
    SketchFeature.addGeometry(
        Part.LineSegment(
            FreeCAD.Vector(69.432587, -53.196629, 0),
            FreeCAD.Vector(-99.230339, -53.196629, 0),
        )
    )
    SketchFeature.addGeometry(
        Part.LineSegment(
            FreeCAD.Vector(-99.230339, -53.196629, 0),
            FreeCAD.Vector(-99.230339, 36.960674, 0),
        )
    )
    # add the constraints
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", 0, 2, 1, 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", 1, 2, 2, 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", 2, 2, 3, 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", 3, 2, 0, 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Horizontal", 0))
    SketchFeature.addConstraint(Sketcher.Constraint("Horizontal", 2))
    SketchFeature.addConstraint(Sketcher.Constraint("Vertical", 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Vertical", 3))
    # add dimensions
    SketchFeature.addConstraint(Sketcher.Constraint("Distance", 1, 81.370787))
    SketchFeature.addConstraint(Sketcher.Constraint("Distance", 0, 187.573036))


def CreateSlotPlateSet(SketchFeature):
    SketchFeature.addGeometry(
        Part.LineSegment(
            App.Vector(60.029362, -30.279360, 0), App.Vector(-120.376335, -30.279360, 0)
        )
    )
    SketchFeature.addConstraint(Sketcher.Constraint("Horizontal", 0))
    SketchFeature.addGeometry(
        Part.LineSegment(
            App.Vector(-120.376335, -30.279360, 0), App.Vector(-70.193062, 38.113884, 0)
        )
    )
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", 0, 2, 1, 1))
    SketchFeature.addGeometry(
        Part.LineSegment(App.Vector(-70.193062, 38.113884, 0), App.Vector(60.241116, 37.478645, 0))
    )
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", 1, 2, 2, 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Horizontal", 2))
    SketchFeature.addGeometry(
        Part.ArcOfCircle(
            Part.Circle(App.Vector(60.039921, 3.811391, 0), App.Vector(0, 0, 1), 35.127132),
            -1.403763,
            1.419522,
        )
    )
    SketchFeature.addConstraint(Sketcher.Constraint("Tangent", 2, 2, 3, 2))
    SketchFeature.addConstraint(Sketcher.Constraint("Tangent", 0, 1, 3, 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Angle", 0, 2, 1, 1, 0.947837))
    SketchFeature.addConstraint(Sketcher.Constraint("Distance", 0, 184.127425))
    SketchFeature.setDatum(7, 200.000000)
    SketchFeature.addConstraint(Sketcher.Constraint("Radius", 3, 38.424808))
    SketchFeature.setDatum(8, 40.000000)
    SketchFeature.setDatum(6, 0.872665)
    SketchFeature.addConstraint(Sketcher.Constraint("DistanceX", 0, 2, 0.0))
    SketchFeature.setDatum(9, 0.000000)
    SketchFeature.movePoint(0, 2, App.Vector(-0.007829, -33.376450, 0))
    SketchFeature.movePoint(0, 2, App.Vector(-0.738149, -10.493386, 0))
    SketchFeature.movePoint(0, 2, App.Vector(-0.007829, 2.165328, 0))
    SketchFeature.addConstraint(Sketcher.Constraint("DistanceY", 0, 2, 2.165328))
    SketchFeature.setDatum(10, 0.000000)


def CreateSlotPlateInnerSet(SketchFeature):
    SketchFeature.addGeometry(
        Part.Circle(App.Vector(195.055893, 39.562252, 0), App.Vector(0, 0, 1), 29.846098)
    )
    SketchFeature.addGeometry(
        Part.LineSegment(App.Vector(150.319031, 13.449363, 0), App.Vector(36.700474, 13.139774, 0))
    )
    SketchFeature.addConstraint(Sketcher.Constraint("Horizontal", 5))
    SketchFeature.addGeometry(
        Part.LineSegment(App.Vector(36.700474, 13.139774, 0), App.Vector(77.566010, 63.292927, 0))
    )
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", 5, 2, 6, 1))
    SketchFeature.addGeometry(
        Part.LineSegment(App.Vector(77.566010, 63.292927, 0), App.Vector(148.151917, 63.602505, 0))
    )
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", 6, 2, 7, 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Horizontal", 7))
    SketchFeature.addConstraint(Sketcher.Constraint("Parallel", 1, 6))
    SketchFeature.addGeometry(
        Part.ArcOfCircle(
            Part.Circle(App.Vector(192.422913, 38.216347, 0), App.Vector(0, 0, 1), 45.315174),
            2.635158,
            3.602228,
        )
    )
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", 7, 2, 8, 1))
    SketchFeature.addConstraint(Sketcher.Constraint("Coincident", 8, 2, 5, 1))


def CreateThreeLinesWithCommonPoint(SketchFeature):
    SketchFeature.addGeometry(
        Part.LineSegment(
            App.Vector(-55.965607, -9.864289, 0), App.Vector(-55.600571, -9.387639, 0)
        ),
        False,
    )
    SketchFeature.addGeometry(
        Part.LineSegment(
            App.Vector(-55.735817, -9.067246, 0), App.Vector(-55.600571, -9.387639, 0)
        ),
        False,
    )
    SketchFeature.addGeometry(
        Part.LineSegment(
            App.Vector(-55.600571, -9.387639, 0), App.Vector(-55.058266, -9.677831, 0)
        ),
        False,
    )


# ---------------------------------------------------------------------------
# define the test cases to test the FreeCAD Sketcher module
# ---------------------------------------------------------------------------


class TestSketcherSolver(unittest.TestCase):
    def setUp(self):
        self.Doc = FreeCAD.newDocument("SketchSolverTest")

    def testBoxCase(self):
        self.Box = self.Doc.addObject("Sketcher::SketchObject", "SketchBox")
        CreateBoxSketchSet(self.Box)
        self.Doc.recompute()
        # moving a point of the sketch
        self.Box.movePoint(0, 2, App.Vector(88.342697, 28.174158, 0))
        # fully constrain
        self.Box.addConstraint(Sketcher.Constraint("DistanceX", 1, 2, 90.0))
        self.Box.addConstraint(Sketcher.Constraint("DistanceY", 1, 2, -50.0))
        self.Doc.recompute()

    def testSlotCase(self):
        self.Slot = self.Doc.addObject("Sketcher::SketchObject", "SketchSlot")
        CreateSlotPlateSet(self.Slot)
        self.Doc.recompute()
        # test if all edges created
        self.assertTrue(len(self.Slot.Shape.Edges) == 4)
        CreateSlotPlateInnerSet(self.Slot)
        self.Doc.recompute()
        self.assertTrue(len(self.Slot.Shape.Edges) == 9)

    def testIssue3245(self):
        self.Doc2 = FreeCAD.newDocument("Issue3245")
        self.Doc2.addObject("Sketcher::SketchObject", "Sketch")
        self.Doc2.Sketch.Placement = App.Placement(
            App.Vector(0.000000, 0.000000, 0.000000),
            App.Rotation(0.000000, 0.000000, 0.000000, 1.000000),
        )
        self.Doc2.Sketch.MapMode = "Deactivated"
        self.Doc2.Sketch.addGeometry(
            Part.LineSegment(
                App.Vector(-1.195999, 56.041161, 0), App.Vector(60.654316, 56.382877, 0)
            ),
            False,
        )
        self.Doc2.Sketch.addConstraint(Sketcher.Constraint("PointOnObject", 0, 1, -2))
        self.Doc2.Sketch.addConstraint(Sketcher.Constraint("Horizontal", 0))
        self.Doc2.Sketch.addGeometry(
            Part.LineSegment(
                App.Vector(0.512583, 32.121155, 0), App.Vector(60.654316, 31.779440, 0)
            ),
            False,
        )
        self.Doc2.Sketch.addConstraint(Sketcher.Constraint("Horizontal", 1))
        self.Doc2.Sketch.addGeometry(
            Part.LineSegment(
                App.Vector(0.170867, 13.326859, 0), App.Vector(61.679455, 13.326859, 0)
            ),
            False,
        )
        self.Doc2.Sketch.addConstraint(Sketcher.Constraint("PointOnObject", 2, 1, -2))
        self.Doc2.Sketch.addConstraint(Sketcher.Constraint("Horizontal", 2))
        self.Doc2.Sketch.addConstraint(Sketcher.Constraint("PointOnObject", 1, 1, -2))
        self.Doc2.Sketch.addConstraint(Sketcher.Constraint("DistanceX", 0, 1, 0, 2, 60.654316))
        self.Doc2.Sketch.setExpression("Constraints[6]", "60")
        self.Doc2.Sketch.addConstraint(Sketcher.Constraint("DistanceX", 1, 1, 1, 2, 60.654316))
        self.Doc2.Sketch.setExpression("Constraints[7]", "65")
        self.Doc2.Sketch.addConstraint(Sketcher.Constraint("DistanceX", 2, 1, 2, 2, 61.679455))
        self.Doc2.Sketch.setExpression("Constraints[8]", "70")
        self.Doc2.recompute()
        self.Doc2.Sketch.delGeometry(2)
        values = d = {key: value for (key, value) in self.Doc2.Sketch.ExpressionEngine}
        self.assertTrue(values["Constraints[4]"] == "60")
        self.assertTrue(values["Constraints[5]"] == "65")
        FreeCAD.closeDocument("Issue3245")

    def testIssue3245_2(self):
        self.Doc2 = FreeCAD.newDocument("Issue3245")
        ActiveSketch = self.Doc2.addObject("Sketcher::SketchObject", "Sketch")
        ActiveSketch.Placement = App.Placement(
            App.Vector(0.000000, 0.000000, 0.000000),
            App.Rotation(0.000000, 0.000000, 0.000000, 1.000000),
        )
        ActiveSketch.MapMode = "Deactivated"
        geoList = []
        geoList.append(
            Part.LineSegment(
                App.Vector(-23.574591, 42.399727, 0),
                App.Vector(81.949776, 42.399727, 0),
            )
        )
        geoList.append(
            Part.LineSegment(
                App.Vector(81.949776, 42.399727, 0),
                App.Vector(81.949776, -19.256901, 0),
            )
        )
        geoList.append(
            Part.LineSegment(
                App.Vector(81.949776, -19.256901, 0),
                App.Vector(-23.574591, -19.256901, 0),
            )
        )
        geoList.append(
            Part.LineSegment(
                App.Vector(-23.574591, -19.256901, 0),
                App.Vector(-23.574591, 42.399727, 0),
            )
        )
        ActiveSketch.addGeometry(geoList, False)
        conList = []
        conList.append(Sketcher.Constraint("Coincident", 0, 2, 1, 1))
        conList.append(Sketcher.Constraint("Coincident", 1, 2, 2, 1))
        conList.append(Sketcher.Constraint("Coincident", 2, 2, 3, 1))
        conList.append(Sketcher.Constraint("Coincident", 3, 2, 0, 1))
        conList.append(Sketcher.Constraint("Horizontal", 0))
        conList.append(Sketcher.Constraint("Horizontal", 2))
        conList.append(Sketcher.Constraint("Vertical", 1))
        conList.append(Sketcher.Constraint("Vertical", 3))
        ActiveSketch.addConstraint(conList)
        ActiveSketch.addConstraint(Sketcher.Constraint("DistanceX", 0, 1, 0, 2, 105.524367))
        ActiveSketch.setExpression("Constraints[8]", "10 + 10")
        ActiveSketch.addConstraint(Sketcher.Constraint("DistanceY", 3, 1, 3, 2, 61.656628))
        ActiveSketch.setDatum(9, App.Units.Quantity("5.000000 mm"))
        ActiveSketch.delConstraint(8)
        values = d = {key: value for (key, value) in self.Doc2.Sketch.ExpressionEngine}
        self.Doc2.recompute()
        self.assertTrue(len(values) == 0)
        FreeCAD.closeDocument("Issue3245")

    def testBlockConstraintEllipse(self):
        self.Doc3 = FreeCAD.newDocument("BlockConstraintTests")
        ActiveSketch = self.Doc3.addObject("Sketcher::SketchObject", "Sketch")
        ActiveSketch.Placement = App.Placement(
            App.Vector(0.000000, 0.000000, 0.000000),
            App.Rotation(0.000000, 0.000000, 0.000000, 1.000000),
        )
        ActiveSketch.MapMode = "Deactivated"
        ActiveSketch.addGeometry(
            Part.Ellipse(
                App.Vector(-19.129438, 14.345055, 0),
                App.Vector(-33.806261, 12.085921, 0),
                App.Vector(-30.689360, 7.107538, 0),
            ),
            False,
        )
        ActiveSketch.solve()
        ActiveSketch.exposeInternalGeometry(0)
        ActiveSketch.solve()
        ActiveSketch.movePoint(0, 0, App.Vector(-26.266434, 14.345055, 0), 0)
        ActiveSketch.solve()
        ActiveSketch.addConstraint(Sketcher.Constraint("Block", 0))  # Block the Ellipse in place
        ActiveSketch.addConstraint(
            Sketcher.Constraint("Block", 1)
        )  # Block the major axis in place (on purpose)
        status = ActiveSketch.solve()
        self.assertTrue(status == 0)  # no redundants/conflicts/convergence issues
        ActiveSketch.addConstraint(
            Sketcher.Constraint("Distance", 1, 27.277350)
        )  # Length of major axis
        ActiveSketch.setDriving(
            6, True
        )  # ensure length is driving (because pre-existing block constraint on major axis)
        ActiveSketch.setDatum(6, App.Units.Quantity("28.000000 mm"))
        status = ActiveSketch.solve()
        self.assertTrue(status == 0)  # no redundants/conflicts/convergence issues
        ActiveSketch.addConstraint(
            Sketcher.Constraint("Distance", 2, 11.747233)
        )  # Length of minor axis
        ActiveSketch.setDatum(7, App.Units.Quantity("15.000000 mm"))
        ActiveSketch.solve()
        self.assertTrue(status == 0)  # no redundants/conflicts/convergence issues
        ActiveSketch.addConstraint(Sketcher.Constraint("Block", 2))
        ActiveSketch.solve()
        self.assertTrue(status == 0)  # no redundants/conflicts/convergence issues
        ActiveSketch.addConstraint(
            Sketcher.Constraint("Horizontal", 1)
        )  # Make major axis horizontal (together with horizontal and length driving constraints)
        ActiveSketch.solve()
        self.assertTrue(status == 0)  # no redundants/conflicts/convergence issues
        ActiveSketch.addConstraint(
            Sketcher.Constraint("DistanceX", 0, 3, -1, 1, 27.655024)
        )  # Locate Ellipse center
        ActiveSketch.addConstraint(Sketcher.Constraint("DistanceY", 0, 3, -1, 1, -20.877021))
        ActiveSketch.setDatum(10, App.Units.Quantity("25.000000 mm"))
        ActiveSketch.setDatum(11, App.Units.Quantity("-20.000000 mm"))
        ActiveSketch.solve()
        self.assertTrue(status == 0)  # no redundants/conflicts/convergence issues
        FreeCAD.closeDocument(self.Doc3.Name)

    def testThreeLinesWithCoincidences_1(self):
        sketch = self.Doc.addObject("Sketcher::SketchObject", "Sketch")
        CreateThreeLinesWithCommonPoint(sketch)
        sketch.addConstraint(Sketcher.Constraint("Coincident", 1, 2, 0, 2))
        sketch.addConstraint(Sketcher.Constraint("Coincident", 2, 1, 0, 2))
        self.assertEqual(sketch.detectMissingPointOnPointConstraints(0.0001), 0)

    # Same as in testThreeLinesWithCoincidences_1 but set the constraints on
    # different lines
    def testThreeLinesWithCoincidences_2(self):
        sketch = self.Doc.addObject("Sketcher::SketchObject", "Sketch")
        CreateThreeLinesWithCommonPoint(sketch)
        sketch.addConstraint(Sketcher.Constraint("Coincident", 1, 2, 0, 2))
        sketch.addConstraint(Sketcher.Constraint("Coincident", 2, 1, 1, 2))
        self.assertEqual(sketch.detectMissingPointOnPointConstraints(0.0001), 0)

    def testCircleToLineDistance_Driving_Passant(self):
        sketch = self.Doc.addObject("Sketcher::SketchObject", "Sketch")
        radius = 20
        circle = Part.Circle(vec(0, 0), xy_normal, radius)
        line = Part.LineSegment(vec(-radius, 2 * radius), vec(radius, 2 * radius))
        c_idx = sketch.addGeometry(circle)
        l_idx = sketch.addGeometry(line)
        # use a positive distance, other than the initial distance of the line
        wanted_distance = radius / 2
        sketch.addConstraint(Sketcher.Constraint("Distance", c_idx, l_idx, wanted_distance))
        self.assertSuccessfulSolve(sketch)
        c_shape = sketch.Geometry[c_idx].toShape()
        l_shape = sketch.Geometry[l_idx].toShape()
        self.assertShapeDistance(c_shape, l_shape, wanted_distance)

    @unittest.skip("Support for secants still under discussion, see comments in PR 9044")
    def testCircleToLineDistance_Driving_Secant(self):
        sketch = self.Doc.addObject("Sketcher::SketchObject", "Sketch")
        radius = 20
        c_idx = sketch.addGeometry(Part.Circle(vec(0, 0), xy_normal, radius))
        l_idx = sketch.addGeometry(
            Part.LineSegment(vec(-radius, 2 * radius), vec(radius, 2 * radius))
        )
        # use a negative distance to tell "line is within the circle"
        wanted_distance = -radius / 2
        sketch.addConstraint(Sketcher.Constraint("Distance", c_idx, l_idx, wanted_distance))
        self.assertSuccessfulSolve(sketch)
        c_shape = sketch.Geometry[c_idx].toShape()
        l_shape = sketch.Geometry[l_idx].toShape()
        self.assertShapeDistance(c_shape, l_shape, 0)  # secant intersects circle, thus no distance

    def testCircleToLineDistance_Reference_Secant(self):
        sketch = self.Doc.addObject("Sketcher::SketchObject", "Sketch")
        radius = 20
        c_idx = sketch.addGeometry(Part.Circle(vec(0, 0), xy_normal, radius))
        l_idx = sketch.addGeometry(
            Part.LineSegment(vec(-radius, radius / 2), vec(radius, radius / 2))
        )
        # The block constraints are required to ensure the geometry does not move.
        # Without this, the solver may find another valid solution than what we assert.
        sketch.addConstraint(
            [Sketcher.Constraint("Block", c_idx), Sketcher.Constraint("Block", l_idx)]
        )
        # use a negative distance to tell "line is within the circle"
        expected_distance = -radius / 2  # note that we don't set this in the constraint below!
        # TODO: addConstraint(constraint) triggers a solve (for godd reasons) however, this way
        # one cannot add non-driving constraints. In contrast, addConstraint(list(constraint))
        # does not solve automatically, thus we use this "overload".
        # Much nicer would be an addConstraint(constraint, isReference=False), like addGeometry
        dist_idx = sketch.addConstraint([Sketcher.Constraint("Distance", c_idx, l_idx, 0)])[0]
        sketch.setDriving(dist_idx, False)
        self.assertSuccessfulSolve(sketch)
        actual_distance = sketch.Constraints[dist_idx].Value
        self.assertAlmostEqual(
            expected_distance,
            actual_distance,
            delta=Precision.confusion(),
            msg="Reference constraint did not return the expected distance.",
        )

    def testRemovedExternalGeometryReference(self):
        body = self.Doc.addObject("PartDesign::Body", "Body")
        sketch = self.Doc.addObject("Sketcher::SketchObject", "Sketch")
        CreateRectangleSketch(sketch, (0, 0), (30, 30))
        pad = self.Doc.addObject("PartDesign::Pad", "Pad")
        pad.Profile = sketch
        sketch1 = self.Doc.addObject("Sketcher::SketchObject", "Sketch1")
        CreateCircleSketch(sketch1, (15, 15), 0.25)
        body.addObject(sketch)
        body.addObject(pad)
        body.addObject(sketch1)
        self.Doc.recompute()
        hole = self.Doc.addObject("PartDesign::Hole", "Hole")
        hole.Refine = True
        body.addObject(hole)
        hole.Profile = sketch1
        hole.Diameter = 0.250000
        hole.Depth = 10.000000
        hole.DrillPointAngle = 118.000000
        hole.TaperedAngle = 90.000000
        hole.Diameter = 6.000000
        hole.Depth = 8.000000
        hole.DrillPointAngle = 118.000000
        hole.TaperedAngle = 90.000000
        hole.Tapered = 0
        hole.Depth = 8.000000
        hole.DrillPointAngle = 118.000000
        hole.Threaded = 1
        hole.ModelThread = 0
        hole.ThreadDepthType = 0
        hole.ThreadType = 1
        hole.ThreadSize = 16
        hole.ThreadClass = 0
        hole.ThreadDirection = 0
        hole.HoleCutType = 0
        hole.DepthType = 0
        hole.DrillPoint = 1
        hole.DrillForDepth = 0
        hole.Tapered = 0
        self.Doc.recompute()
        self.assertEqual(len(hole.Shape.Edges), 13)
        hole.Threaded = True
        hole.ModelThread = True
        body.addObject(hole)
        #
        sketch2 = self.Doc.addObject("Sketcher::SketchObject", "Sketch2")
        CreateRectangleSketch(sketch2, (0, 0), (3, 3))
        body.addObject(sketch2)
        self.Doc.recompute()
        sketch2.addExternal("Hole", "Edge29")  # Edge29 will disappear when we stop modeling threads
        self.assertEqual(len(hole.Shape.Edges), 38)
        hole.ModelThread = False
        hole.Refine = True
        self.Doc.recompute()
        self.assertEqual(len(hole.Shape.Edges), 38)
        self.assertEqual(len(sketch2.ExternalGeometry), 1)

    def testSaveLoadWithExternalGeometryReference(self):
        # Arrange
        body = self.Doc.addObject("PartDesign::Body", "Body")
        sketch = self.Doc.addObject("Sketcher::SketchObject", "Sketch")
        CreateRectangleSketch(sketch, (0, 0), (30, 30))
        pad = self.Doc.addObject("PartDesign::Pad", "Pad")
        pad.Profile = sketch
        sketch1 = self.Doc.addObject("Sketcher::SketchObject", "Sketch1")
        body.addObject(sketch)
        body.addObject(pad)
        body.addObject(sketch1)
        self.Doc.recompute()
        sketch1.addExternal("Pad", "Edge12")
        self.Doc.recompute()

        # Act: Try changing sketch before the save
        sketch = self.Doc.getObject("Sketch")
        g1 = sketch.Constraints[11].First
        d1 = sketch.Constraints[11].Value
        sketch.delConstraint(11)
        sketch.addConstraint(Sketcher.Constraint("Distance", g1, d1 - 1.0))
        self.Doc.recompute()

        # Act: Save and reload the file
        filename = self.Doc.Name + ".FCStd"
        self.Doc.saveAs(filename)
        FreeCAD.closeDocument(self.Doc.Name)
        self.Doc = FreeCAD.openDocument(filename)
        pad = self.Doc.getObject("Pad")
        sketch1 = self.Doc.getObject("Sketch1")
        self.Doc.recompute()

        # Act:  change sketch after restore ( trigger missing references if there is a bug )
        sketch = self.Doc.getObject("Sketch")
        g1 = sketch.Constraints[11].First
        d1 = sketch.Constraints[11].Value
        sketch.delConstraint(11)
        sketch.addConstraint(Sketcher.Constraint("Distance", g1, d1 - 1.0))
        self.Doc.recompute()

        # Assert
        self.assertEqual(pad.Shape.ElementMapSize, 30)
        self.assertIn("Edge12", pad.Shape.ElementReverseMap)
        self.assertIn((pad, ("Edge12",)), sketch1.ExternalGeometry)  # Not "?Edge12"

    def testTNPExternalGeometryStored(self):
        # Arrange
        import xml.etree.ElementTree as ET

        sketch = self.Doc.addObject("Sketcher::SketchObject", "Sketch")
        CreateRectangleSketch(sketch, (0, 0), (30, 30))
        sketch1 = self.Doc.addObject("Sketcher::SketchObject", "Sketch1")
        pad = self.Doc.addObject("PartDesign::Pad", "Pad")
        pad.Profile = sketch
        self.Doc.recompute()
        sketch1.addExternal("Pad", "Edge12")
        self.Doc.recompute()
        # Act
        root = ET.fromstring("<all>" + sketch1.Content + "</all>")
        # Can't use != in an xpath because it wasn't added until python 3.10.
        # "*/*[@name='ExternalGeo']//*/[@type='Sketcher::ExternalGeometryExtension']/[@Ref!='']"
        extRefs = root.findall(
            "*/*[@name='ExternalGeo']//*/[@type='Sketcher::ExternalGeometryExtension']/[@Ref='']"
        )
        extRefsAll = root.findall(
            "*/*[@name='ExternalGeo']//*/[@type='Sketcher::ExternalGeometryExtension']/[@Ref]"
        )
        # Assert
        self.assertEqual(len(extRefs), 2)
        self.assertEqual(len(extRefsAll), 3)
        self.assertEqual(root.tag, "all")
        # Act
        filename = self.Doc.Name + ".FCStd"
        self.Doc.saveAs(filename)
        FreeCAD.closeDocument(self.Doc.Name)
        self.Doc = FreeCAD.openDocument(filename)
        # Assert
        root = ET.fromstring("<all>" + self.Doc.getObject("Sketch1").Content + "</all>")
        extRefs = root.findall(
            "*/*[@name='ExternalGeo']//*/[@type='Sketcher::ExternalGeometryExtension']/[@Ref='']"
        )
        extRefsAll = root.findall(
            "*/*[@name='ExternalGeo']//*/[@type='Sketcher::ExternalGeometryExtension']/[@Ref]"
        )
        self.assertEqual(len(extRefs), 2)
        self.assertEqual(len(extRefsAll), 3)
        self.assertEqual(root.tag, "all")
        # Act to change the constraint
        sketch = self.Doc.getObject("Sketch")
        g1 = sketch.Constraints[11].First
        d1 = sketch.Constraints[11].Value
        sketch.delConstraint(11)
        sketch.addConstraint(Sketcher.Constraint("Distance", g1, d1 - 1.0))
        self.Doc.recompute()
        # Assert
        root = ET.fromstring("<all>" + self.Doc.getObject("Sketch1").Content + "</all>")
        extRefs = root.findall(
            "*/*[@name='ExternalGeo']//*/[@type='Sketcher::ExternalGeometryExtension']/[@Ref='']"
        )
        extRefsAll = root.findall(
            "*/*[@name='ExternalGeo']//*/[@type='Sketcher::ExternalGeometryExtension']/[@Ref]"
        )
        self.assertEqual(len(extRefs), 2)
        self.assertEqual(len(extRefsAll), 3)
        self.assertEqual(root.tag, "all")

    def testConstructionToggleTNP(self):
        """Bug 15484"""
        # Arrange
        import xml.etree.ElementTree as ET

        body = self.Doc.addObject("PartDesign::Body", "Body")
        sketch = self.Doc.addObject("Sketcher::SketchObject", "Sketch")
        CreateRectangleSketch(sketch, (0, 0), (30, 30))
        # Add zigsag geo as construction lines
        i = sketch.GeometryCount
        sketch.addGeometry(
            Part.LineSegment(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(-10, 10, 0)), True
        )
        sketch.addGeometry(
            Part.LineSegment(FreeCAD.Vector(-10, 10, 0), FreeCAD.Vector(-5, 20, 0)), True
        )
        sketch.addGeometry(
            Part.LineSegment(FreeCAD.Vector(-5, 20, 0), FreeCAD.Vector(-10, 25, 0)), True
        )
        sketch.addGeometry(
            Part.LineSegment(FreeCAD.Vector(-10, 25, 0), FreeCAD.Vector(0, 30, 0)), True
        )
        sketch.addConstraint(Sketcher.Constraint("Coincident", i + 0, 2, i + 1, 1))
        sketch.addConstraint(Sketcher.Constraint("Coincident", i + 1, 2, i + 2, 1))
        sketch.addConstraint(Sketcher.Constraint("Coincident", i + 2, 2, i + 3, 1))
        sketch.addConstraint(Sketcher.Constraint("Coincident", i + 3, 2, 0, 1))
        sketch.addConstraint(Sketcher.Constraint("Coincident", i + 0, 1, 2, 2))

        pad = self.Doc.addObject("PartDesign::Pad", "Pad")
        pad.Profile = sketch
        body.addObject(sketch)
        body.addObject(pad)
        self.Doc.recompute()
        sketch1 = self.Doc.addObject("Sketcher::SketchObject", "Sketch1")
        sketch1.AttachmentSupport = (pad, ("Face6"))
        sketch1.MapMode = "FlatFace"
        self.Doc.recompute()

        CreateCircleSketch(sketch1, (2, 2, 0), 1)
        CreateCircleSketch(sketch1, (6, 2, 0), 1)
        body.addObject(sketch1)
        self.Doc.recompute()
        # Act toggle construction lines on in sketch; pad now has 9 instead of 6 faces.
        sketch.setConstruction(4, False)
        sketch.setConstruction(5, False)
        sketch.setConstruction(6, False)
        sketch.setConstruction(7, False)
        sketch.setConstruction(3, True)
        self.Doc.recompute()
        # Assert
        # AttachmentSupport is a list of (object,(subobject list)) with 1 entry.  Get the
        # first and only subobject name in second part of that first tuple and see that it moved
        # from the Face6 we set above.
        self.assertEqual(sketch1.AttachmentSupport[0][1][0], "Face9")
        self.assertIn("Face6", pad.Shape.ElementReverseMap)  # different Face6 exists

    # TODO other tests:
    #  getHigherElement

    def assertSuccessfulSolve(self, sketch, msg=None):
        status = sketch.solve()
        # TODO: can we get the solver's messages somehow to improve the message?
        self.assertTrue(status == 0, msg=msg or "solver didn't converge")

    def assertShapeDistance(self, shape1, shape2, expected_distance, msg=None):
        distance, _, _ = shape1.distToShape(shape2)
        self.assertAlmostEqual(
            distance,
            expected_distance,
            delta=Precision.confusion(),
            msg=msg or "The given shapes are not spaced by the expected distance.",
        )

    def tearDown(self):
        # closing doc
        FreeCAD.closeDocument("SketchSolverTest")
        # print ("omit closing document for debugging")
