#
# FreeCAD macro ViewRotation.
# This GUI allows the view to be rotated with more precision than when using
# the mouse. Rotation is according to axes fixed with respect to the user and
# not the objects, though the aim is that the objects rotate about their
# approximate shared centre rather than the view centre.
# The GUI defaults to the top right of the screen, this behaviour can be
# changed by editing.
# [http://forum.freecadweb.org/viewtopic.php?f=3&t=1784&hilit=View+Rotation#p12012 View+Rotation]


__Name__ = 'View Rotation'
__Comment__ = 'This GUI allows the view to be rotated precisely'
__Author__ = 'Joe Dowsett'
__Version__ = '1.0.1'
__Date__ = '2024-11-22'
__License__ = 'CC-BY-3.0'
__Web__ = 'https://www.freecadweb.org/wiki/Macro_View_Rotation'
__Wiki__ = 'https://www.freecadweb.org/wiki/Macro_View_Rotation'
__Icon__ = ''
__Help__ = 'Rotation is according to axes fixed with respect to the user.'
__Status__ = ''
__Requires__ = ''
__Files__ = 'ViewRotationOut.png,ViewRotationRight.png,ViewRotationUp.png'

from math import pi
from functools import partial
import os

from PySide import QtCore  # FreeCAD's PySide!
from PySide import QtGui  # FreeCAD's PySide!
from pivy import coin

import FreeCAD as app
import FreeCADGui as gui


def find_centre():
    doc = app.activeDocument()
    if doc is None:
        return app.Vector(0.0, 0.0, 0.0)

    xmax = 0.0
    xmin = 0.0
    ymax = 0.0
    ymin = 0.0
    zmax = 0.0
    zmin = 0.0
    for obj in doc.Objects:
        try:
            if obj.TypeId[:4] == 'Mesh':
                box = obj.Mesh.BoundBox
            elif obj.TypeId[:6] == 'Points':
                box = obj.Points.BoundBox
            elif obj.TypeId[:4] == 'Part':
                box = obj.Shape.BoundBox
            else:
                continue
        except AttributeError:
            continue
        xmax = max(xmax, box.XMax)
        xmin = min(xmin, box.XMin)
        ymax = max(ymax, box.YMax)
        ymin = min(ymin, box.YMin)
        zmax = max(zmax, box.ZMax)
        zmin = min(zmin, box.ZMin)

    return app.Vector((xmax + xmin) / 2, (ymax + ymin) / 2, (zmax + zmin) / 2)


class RotateGui(QtGui.QWidget):
    def __init__(self):
        super().__init__()
        self.init_ui()
        self.init_rotate()

    def init_ui(self):
        macro_dir = app.getUserMacroDir(True)
        self.sliders = []
        self.line_edits = []

        vbox = QtGui.QVBoxLayout()

        icons = (
                'ViewRotationRight.png',
                'ViewRotationUp.png',
                'ViewRotationOut.png',
        )
        for i, icon in enumerate(icons):
            slider = QtGui.QSlider(QtCore.Qt.Horizontal, self)
            slider.setFocusPolicy(QtCore.Qt.NoFocus)
            slider.setSingleStep(5)
            slider.setPageStep(15)
            slider.setValue(0)
            slider.setMaximum(180)
            slider.setMinimum(-180)
            slider.valueChanged.connect(partial(self.on_slider_value_changed, i))
            self.sliders.append(slider)

            line_edit = QtGui.QLineEdit(self)
            line_edit.setText('0')
            line_edit.setAlignment(QtCore.Qt.AlignRight)
            line_edit.returnPressed.connect(self.valueEntered)
            self.line_edits.append(line_edit)

            label = QtGui.QLabel(self)
            label.setPixmap(QtGui.QPixmap(os.path.join(macro_dir, icon)))

            hbox = QtGui.QHBoxLayout()
            hbox.addWidget(label, 1, QtCore.Qt.AlignCenter)
            hbox.addWidget(slider, 4)
            hbox.addWidget(line_edit, 1)
            vbox.addLayout(hbox)

        reset_button = QtGui.QPushButton('Reset')
        reset_button.clicked.connect(self.reset)

        ok_button = QtGui.QPushButton('OK')
        ok_button.clicked.connect(self.close)

        cancel_button = QtGui.QPushButton('Cancel')
        cancel_button.clicked.connect(self.cancel)

        hbox = QtGui.QHBoxLayout()
        hbox.addWidget(reset_button, 1)
        hbox.addWidget(ok_button, 1)
        hbox.addWidget(cancel_button, 1)
        vbox.addStretch(1)
        vbox.addLayout(hbox)

        self.setLayout(vbox)

        qapp = QtCore.QCoreApplication.instance()
        screen_number = QtGui.QDesktopWidget().screenNumber(self)
        right = qapp.screens()[screen_number].geometry().width()

        self.setGeometry(right - 300, 0, 300, 150)
        self.setWindowTitle('Rotate view')
        self.show()

    def init_rotate(self):
        # Index in [right, up, out]
        self._current_axis = 0

        self.cam = gui.activeDocument().ActiveView.getCameraNode()
        self.centre = coin.SbVec3f(find_centre())
        self.view = self.cam.orientation.getValue()
        self.pos = self.cam.position.getValue()

        # Store a copy of the original view to be restored in the case of user
        # selecting Reset or Cancel.
        self.original_view = coin.SbRotation(self.view.getValue())
        self.original_pos = coin.SbVec3f(self.pos.getValue())

        self.config_direction(0)

    def reset(self):
        # Reset the view to the original one.
        self.cam.orientation = self.original_view
        self.cam.position = self.original_pos
        for slider in self.sliders:
            # Deactivate callbacks.
            slider.blockSignals(True)
            slider.setValue(0)
            slider.blockSignals(False)
        for tbox in self.line_edits:
            tbox.setText('0')
        self.config_direction(0)

    def cancel(self):
        self.reset()
        self.close()

    def config_direction(self, axis_index):
        """
        Assign `self.direction` to either the right, up or out vector.

        Evaluate the vectors corresponding to the three directions for the
        current view, and assign `self.direction` to the one at index `axis_index`.

        """
        self.view = self.cam.orientation.getValue()
        self.view = coin.SbRotation(self.view.getValue())
        self.pos = self.cam.position.getValue()
        self.pos = coin.SbVec3f(self.pos.getValue())

        up = coin.SbVec3f(0, 1, 0)
        self.up = self.view.multVec(up)
        out = coin.SbVec3f(0, 0, 1)
        self.out = self.view.multVec(out)
        u = self.up.getValue()
        o = self.out.getValue()
        r = (u[1]*o[2]-u[2]*o[1], u[2]*o[0]-u[0]*o[2], u[0]*o[1]-u[1]*o[0])
        self.right = coin.SbVec3f(r)

        self.direction = [self.right, self.up, self.out][axis_index]

    def check(self, axis_index):
        """
        Check if the direction of rotation has changed.

        Check if the direction of rotation has changed, if so then set
        previous slider and textbox to zero, and setup the new direction.

        """
        if axis_index != self._current_axis:
            # Deactivate callbacks.
            self.sliders[self._current_axis].blockSignals(True)
            self.sliders[self._current_axis].setValue(0)
            self.sliders[self._current_axis].blockSignals(False)
            self.line_edits[self._current_axis].blockSignals(True)
            self.line_edits[self._current_axis].setText("0")
            self.line_edits[self._current_axis].blockSignals(False)
            self._current_axis = axis_index
            self.config_direction(axis_index)

    def rotate(self, value):
        # Carry out the desired rotation about self.direction.
        val = value * pi / 180.0
        rot = coin.SbRotation(self.direction, -val)
        nrot = self.view*rot
        prot = rot.multVec(self.pos - self.centre) + self.centre
        self.cam.orientation = nrot
        self.cam.position = prot

    def on_slider_value_changed(self, slider_index: int, value: int):
        """
        Respond to the change in value of a slider.

        Respond to the change in value of a slider, update the corresponding
        text box, check for a direction change then rotate
        if the value was changed internally, ignore event.

        """
        self.line_edits[slider_index].setText(str(value))
        self.check(slider_index)
        self.rotate(value)

    def valueEntered(self):
        """
        Respond to a value being entered in a text box.

        Respond to a value being entered in a text box, updating the
        corresponding slider, checking for direction change then rotate.

        """
        sender = self.sender()
        for i in range(3):
            if sender == self.line_edits[i]:
                break
        value = round(float(self.line_edits[i].text()))
        self.sliders[i].blockSignals(True)
        self.sliders[i].setValue(value)
        self.sliders[i].blockSignals(False)
        self.line_edits[i].blockSignals(True)
        self.line_edits[i].setText(str(value))
        self.line_edits[i].blockSignals(False)
        self.check(i)
        self.rotate(value)


if __name__ == '__main__':
    # We need to set a variable, otherwise, the dialog doesn't appear.
    rotate = RotateGui()