__Name__ = 'Measure Circle'
__Comment__ = 'Compute the radius of a circle by 3 points or a circular edge'
__Author__ = 'peepsalot, galou_breizh'
__Version__ = '0.10.1'
__Date__ = '2023-03-11'
__License__ = 'LGPL-2.0-or-later'
__Web__ = 'http://www.freecadweb.org/wiki/Macro_MeasureCircle'
__Wiki__ = 'http://www.freecadweb.org/wiki/Macro_MeasureCircle'
__Icon__ = 'MeasureCircle.png'
__Help__ = 'Make a selection of 3 points or an edge and launch, or launch first'
__Status__ = 'Beta'
__Requires__ = 'FreeCAD >= 0.18'
__Communication__ = ''
__Files__ = 'MeasureCircle.png'

import FreeCAD as app
import FreeCADGui as gui
import Part


def get_global_placement(obj):
    try:
        # If obj is part of a Part or Body.
        return obj.getParentGeoFeatureGroup().getGlobalPlacement()
    except AttributeError:
        return app.Placement()


class MeasureCircle:
    """Report the radius and center of a circle

    This class will report the computed radius and center of a circle given 3
    vertices or a circular edge.

    Just select the vertices and the result will be shown in the Report View.
    Edges may also be selected and count as two vertices.
    A circular edge can also be selected.
    If two edges are selected the end vertex of the second edge is not
    used in the calculation.
    """
    def __init__(self):
        self.points = []
        self.curve = None
        self.observer_needed = False
        self.get_elements_from_selection_ex()
        # We add the observer only if necessary.
        self.observer_needed = (self.curve is None)
        if self.observer_needed:
            gui.Selection.addObserver(self)
            app.Console.PrintMessage(
                'Select {} vertices on a circle\n'.format(
                    3 - len(self.points)))

    def addSelection(self, doc, obj, sub, pos):
        """Callback of the selection observer

        Do not rename.
        """
        o = app.getDocument(doc).getObject(obj)
        # Get the shape of selected object, if any.
        try:
            shape = o.Shape
        except AttributeError:
            app.Console.PrintMessage('Object is not a shape\n')
            return
        # Get the selected subshape.
        try:
            subshape = getattr(shape, sub)
        except:
            app.Console.PrintMessage(
                '"{}" is not a subshape of "{}"\n'.format(sub, shape))
            return
        if subshape.ShapeType == 'Vertex':
            self.add_point(o, subshape)
        elif subshape.ShapeType == 'Edge':
            self.on_edge(o, subshape)
        # Exit if we have enough points.
        if len(self.points) >= 3:
            self.on_three_points()

    def get_elements_from_selection_ex(self):
        for sel_object in gui.Selection.getSelectionEx():
            obj = sel_object.Object
            for subobject in sel_object.SubObjects:
                if type(subobject) == Part.Vertex:
                    self.add_point(obj, subobject)
                elif type(subobject) == Part.Edge:
                    self.on_edge(obj, subobject)
                # Exit if we have enough points.
                if len(self.points) >= 3:
                    self.on_three_points()
                    return

    def add_point(self, obj, v):
        """Add a point at the location of the selected vertex

        obj is the object the vertex belongs to. If obj belongs to a body, the
        body placement must be taken into account.
        """
        placement = get_global_placement(obj)
        point = placement.multVec(v.Point)
        if point in self.points:
            # Don't allow duplicate points.
            app.Console.PrintWarning('Point already chosen, ignoring\n')
            return
        self.points.append(point)

    def on_edge(self, obj, edge):
        """Add either two points or a circle

        obj is the object the edge belongs to. If obj belongs to a body, the
        body placement must be taken into account.
        """
        curve_type = type(edge.Curve)
        if curve_type == Part.Circle:
            self.on_circle(obj, edge)
        elif curve_type == Part.LineSegment:
            self.add_point(obj, edge.Vertexes[0])
            self.add_point(obj, edge.Vertexes[1])

    def finish(self):
        if self.curve:
            c = self.curve
            # line = Part.makeLine(c.Center, c.Center + c.XAxis * c.Radius)
            # obj = app.ActiveDocument.addObject('Part::Feature', 'Radius')
            # obj.Shape = line
            app.Console.PrintMessage('Diameter: {} mm\n'.format(2 * c.Radius))
            app.Console.PrintMessage('Radius: {} mm\n'.format(c.Radius))
            app.Console.PrintMessage(
                'Center: (X, Y, Z) = ({}, {}, {})\n'.format(
                    *list(c.Location)))
        if self.observer_needed:
            gui.Selection.removeObserver(self)

    def on_circle(self, obj, edge):
        """Add a circle and finish

        obj is the object the curve belongs to. If obj belongs to a body, the
        body placement must be taken into account.
        """
        placement = get_global_placement(obj)
        edge = edge.copy()
        edge.Placement = edge.Placement.multiply(placement)
        self.curve = edge.Curve
        self.finish()

    def on_three_points(self):
        v1 = self.points[0]
        v2 = self.points[1]
        v3 = self.points[2]
        self.curve = Part.Arc(v1, v2, v3).toShape().Curve
        self.finish()

if __name__ == '__main__':
    MeasureCircle()
