rational-linkages 2.2.0__cp310-cp310-musllinux_1_2_aarch64.whl → 2.2.3__cp310-cp310-musllinux_1_2_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rational_linkages/MotionDesigner.py +527 -530
- rational_linkages/PlotterPyqtgraph.py +535 -528
- rational_linkages/utils_rust.cpython-310-aarch64-linux-gnu.so +0 -0
- {rational_linkages-2.2.0.dist-info → rational_linkages-2.2.3.dist-info}/METADATA +1 -2
- {rational_linkages-2.2.0.dist-info → rational_linkages-2.2.3.dist-info}/RECORD +8 -8
- {rational_linkages-2.2.0.dist-info → rational_linkages-2.2.3.dist-info}/WHEEL +0 -0
- {rational_linkages-2.2.0.dist-info → rational_linkages-2.2.3.dist-info}/licenses/LICENSE +0 -0
- {rational_linkages-2.2.0.dist-info → rational_linkages-2.2.3.dist-info}/top_level.txt +0 -0
@@ -611,543 +611,550 @@ class PlotterPyqtgraph:
|
|
611
611
|
event.accept()
|
612
612
|
|
613
613
|
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
self.text_overlay.show()
|
626
|
-
|
627
|
-
def resizeEvent(self, event):
|
628
|
-
super().resizeEvent(event)
|
629
|
-
if hasattr(self, 'text_overlay'):
|
614
|
+
if gl is not None:
|
615
|
+
class CustomGLViewWidget(gl.GLViewWidget):
|
616
|
+
def __init__(self, white_background=False, *args, **kwargs):
|
617
|
+
super().__init__(*args, **kwargs)
|
618
|
+
self.labels = []
|
619
|
+
self.white_background = white_background
|
620
|
+
# Create an overlay widget for displaying text
|
621
|
+
self.text_overlay = QtWidgets.QWidget(self)
|
622
|
+
self.text_overlay.setAttribute(
|
623
|
+
QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
624
|
+
self.text_overlay.setStyleSheet("background:transparent;")
|
630
625
|
self.text_overlay.resize(self.size())
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
mvp = projection_matrix * view_matrix
|
680
|
-
|
681
|
-
# Draw all labels
|
682
|
-
for entry in self.labels:
|
683
|
-
point = entry['point']
|
684
|
-
text = entry['text']
|
685
|
-
|
686
|
-
projected = mvp.map(self._obtain_label_vec(point))
|
687
|
-
if projected.w() != 0:
|
688
|
-
ndc_x = projected.x() / projected.w()
|
689
|
-
ndc_y = projected.y() / projected.w()
|
690
|
-
# Check if the point is in front of the camera
|
691
|
-
if projected.z() / projected.w() < 1.0:
|
692
|
-
x = int((ndc_x * 0.5 + 0.5) * self.width())
|
693
|
-
y = int((1 - (ndc_y * 0.5 + 0.5)) * self.height())
|
694
|
-
painter.drawText(x, y, text)
|
695
|
-
|
696
|
-
painter.end()
|
697
|
-
|
698
|
-
def showEvent(self, event):
|
699
|
-
super().showEvent(event)
|
700
|
-
self.text_overlay.installEventFilter(self)
|
701
|
-
|
702
|
-
def eventFilter(self, obj, event):
|
703
|
-
if obj is self.text_overlay and event.type() == QtCore.QEvent.Type.Paint:
|
704
|
-
self.paintOverlay(event)
|
705
|
-
return True
|
706
|
-
return super().eventFilter(obj, event)
|
707
|
-
|
708
|
-
|
709
|
-
class FramePlotHelper:
|
710
|
-
def __init__(self,
|
711
|
-
transform: TransfMatrix = TransfMatrix(),
|
712
|
-
width: float = 2.,
|
713
|
-
length: float = 1.,
|
714
|
-
antialias: bool = True):
|
715
|
-
"""
|
716
|
-
Create a coordinate frame using three GLLinePlotItems.
|
717
|
-
|
718
|
-
:param TransfMatrix transform: The initial transformation matrix.
|
719
|
-
:param float width: The width of the lines.
|
720
|
-
:param float length: The length of the axes.
|
721
|
-
:param bool antialias: Whether to use antialiasing
|
722
|
-
"""
|
723
|
-
# Create GLLinePlotItems for the three axes.
|
724
|
-
# The initial positions are placeholders; they will be set properly in setData().
|
725
|
-
self.x_axis = gl.GLLinePlotItem(pos=np.zeros((2, 3)),
|
726
|
-
color=(1, 0, 0, 0.5),
|
727
|
-
glOptions='translucent',
|
728
|
-
width=width,
|
729
|
-
antialias=antialias)
|
730
|
-
self.y_axis = gl.GLLinePlotItem(pos=np.zeros((2, 3)),
|
731
|
-
color=(0, 1, 0, 0.5),
|
732
|
-
glOptions='translucent',
|
733
|
-
width=width,
|
734
|
-
antialias=antialias)
|
735
|
-
self.z_axis = gl.GLLinePlotItem(pos=np.zeros((2, 3)),
|
736
|
-
color=(0, 0, 1, 0.5),
|
737
|
-
glOptions='translucent',
|
738
|
-
width=width,
|
739
|
-
antialias=antialias)
|
740
|
-
|
741
|
-
# Set the initial transformation
|
742
|
-
self.tr = transform
|
743
|
-
self.length = length
|
744
|
-
self.setData(transform)
|
745
|
-
|
746
|
-
def setData(self, transform: TransfMatrix):
|
747
|
-
"""
|
748
|
-
Update the coordinate frame using a new 4x4 transformation matrix.
|
749
|
-
|
750
|
-
:param TransfMatrix transform: The new transformation matrix.
|
751
|
-
"""
|
752
|
-
self.tr = transform
|
753
|
-
|
754
|
-
# Update the positions for each axis.
|
755
|
-
self.x_axis.setData(pos=np.array([transform.t, transform.t + self.length * transform.n]))
|
756
|
-
self.y_axis.setData(pos=np.array([transform.t, transform.t + self.length * transform.o]))
|
757
|
-
self.z_axis.setData(pos=np.array([transform.t, transform.t + self.length * transform.a]))
|
758
|
-
|
759
|
-
def addToView(self, view: gl.GLViewWidget):
|
760
|
-
"""
|
761
|
-
Add all three axes to a GLViewWidget.
|
762
|
-
|
763
|
-
:param gl.GLViewWidget view: The view to add the axes to.
|
764
|
-
"""
|
765
|
-
view.addItem(self.x_axis)
|
766
|
-
view.addItem(self.y_axis)
|
767
|
-
view.addItem(self.z_axis)
|
768
|
-
|
769
|
-
|
770
|
-
class InteractivePlotterWidget(QtWidgets.QWidget):
|
771
|
-
"""
|
772
|
-
A QWidget that contains a PlotterPyqtgraph 3D view and interactive controls.
|
773
|
-
|
774
|
-
Containts (sliders and text boxes) for plotting and manipulating a mechanism.
|
775
|
-
"""
|
776
|
-
def __init__(self,
|
777
|
-
mechanism: RationalMechanism,
|
778
|
-
base=None,
|
779
|
-
show_tool: bool = True,
|
780
|
-
steps: int = 1000,
|
781
|
-
joint_sliders_lim: float = 1.0,
|
782
|
-
arrows_length: float = 1.0,
|
783
|
-
white_background: bool = False,
|
784
|
-
parent=None,
|
785
|
-
parent_app=None):
|
786
|
-
super().__init__(parent)
|
787
|
-
self.setMinimumSize(800, 600)
|
788
|
-
|
789
|
-
self.mechanism = mechanism
|
790
|
-
self.show_tool = show_tool
|
791
|
-
self.steps = steps
|
792
|
-
self.joint_sliders_lim = joint_sliders_lim
|
793
|
-
self.arrows_length = arrows_length
|
794
|
-
|
795
|
-
if base is not None:
|
796
|
-
if isinstance(base, TransfMatrix):
|
797
|
-
if not base.is_rotation():
|
798
|
-
raise ValueError("Given matrix is not proper rotation.")
|
799
|
-
self.base = base
|
800
|
-
self.base_arr = self.base.array()
|
801
|
-
elif isinstance(base, DualQuaternion):
|
802
|
-
self.base = TransfMatrix(base.dq2matrix())
|
803
|
-
self.base_arr = self.base.array()
|
626
|
+
self.text_overlay.show()
|
627
|
+
|
628
|
+
def resizeEvent(self, event):
|
629
|
+
super().resizeEvent(event)
|
630
|
+
if hasattr(self, 'text_overlay'):
|
631
|
+
self.text_overlay.resize(self.size())
|
632
|
+
|
633
|
+
def add_label(self, point, text):
|
634
|
+
"""Adds a label for a 3D point."""
|
635
|
+
self.labels.append({'point': point, 'text': text})
|
636
|
+
self.update()
|
637
|
+
|
638
|
+
def paintEvent(self, event):
|
639
|
+
# Only handle standard OpenGL rendering here - no mixing with QPainter
|
640
|
+
super().paintEvent(event)
|
641
|
+
|
642
|
+
# Schedule label painting as a separate operation
|
643
|
+
QtCore.QTimer.singleShot(0, self.update_text_overlay)
|
644
|
+
|
645
|
+
def update_text_overlay(self):
|
646
|
+
"""Update the text overlay with current labels"""
|
647
|
+
# Create a new painter for the overlay widget
|
648
|
+
self.text_overlay.update()
|
649
|
+
|
650
|
+
def _obtain_label_vec(self, pt):
|
651
|
+
"""Obtain the label vector."""
|
652
|
+
# Convert the 3D point to homogeneous coordinates
|
653
|
+
if isinstance(pt, np.ndarray):
|
654
|
+
point_vec = pt
|
655
|
+
elif isinstance(pt, PointHomogeneous):
|
656
|
+
point_vec = [pt.coordinates_normalized[1],
|
657
|
+
pt.coordinates_normalized[2],
|
658
|
+
pt.coordinates_normalized[3]]
|
659
|
+
elif isinstance(pt, TransfMatrix):
|
660
|
+
point_vec = [pt.t[0], pt.t[1], pt.t[2]]
|
661
|
+
elif isinstance(pt, FramePlotHelper):
|
662
|
+
point_vec = [pt.tr.t[0], pt.tr.t[1], pt.tr.t[2]]
|
663
|
+
else: # is pyqtgraph marker (scatter)
|
664
|
+
point_vec = [pt.pos[0][0], pt.pos[0][1], pt.pos[0][2]]
|
665
|
+
|
666
|
+
return QtGui.QVector4D(point_vec[0], point_vec[1], point_vec[2], 1.0)
|
667
|
+
|
668
|
+
# This method renders text on the overlay
|
669
|
+
def paintOverlay(self, event):
|
670
|
+
painter = QtGui.QPainter(self.text_overlay)
|
671
|
+
painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
|
672
|
+
if self.white_background:
|
673
|
+
painter.setPen(QtGui.QColor(QtCore.Qt.GlobalColor.black))
|
804
674
|
else:
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
675
|
+
painter.setPen(QtGui.QColor(QtCore.Qt.GlobalColor.white))
|
676
|
+
|
677
|
+
# Get the Model-View-Projection matrix
|
678
|
+
projection_matrix = self.projectionMatrix()
|
679
|
+
view_matrix = self.viewMatrix()
|
680
|
+
mvp = projection_matrix * view_matrix
|
681
|
+
|
682
|
+
# Draw all labels
|
683
|
+
for entry in self.labels:
|
684
|
+
point = entry['point']
|
685
|
+
text = entry['text']
|
686
|
+
|
687
|
+
projected = mvp.map(self._obtain_label_vec(point))
|
688
|
+
if projected.w() != 0:
|
689
|
+
ndc_x = projected.x() / projected.w()
|
690
|
+
ndc_y = projected.y() / projected.w()
|
691
|
+
# Check if the point is in front of the camera
|
692
|
+
if projected.z() / projected.w() < 1.0:
|
693
|
+
x = int((ndc_x * 0.5 + 0.5) * self.width())
|
694
|
+
y = int((1 - (ndc_y * 0.5 + 0.5)) * self.height())
|
695
|
+
painter.drawText(x, y, text)
|
696
|
+
|
697
|
+
painter.end()
|
698
|
+
|
699
|
+
def showEvent(self, event):
|
700
|
+
super().showEvent(event)
|
701
|
+
self.text_overlay.installEventFilter(self)
|
702
|
+
|
703
|
+
def eventFilter(self, obj, event):
|
704
|
+
if obj is self.text_overlay and event.type() == QtCore.QEvent.Type.Paint:
|
705
|
+
self.paintOverlay(event)
|
706
|
+
return True
|
707
|
+
return super().eventFilter(obj, event)
|
708
|
+
else:
|
709
|
+
CustomGLViewWidget = None
|
710
|
+
|
711
|
+
|
712
|
+
if gl is not None:
|
713
|
+
class FramePlotHelper:
|
714
|
+
def __init__(self,
|
715
|
+
transform: TransfMatrix = TransfMatrix(),
|
716
|
+
width: float = 2.,
|
717
|
+
length: float = 1.,
|
718
|
+
antialias: bool = True):
|
719
|
+
"""
|
720
|
+
Create a coordinate frame using three GLLinePlotItems.
|
721
|
+
|
722
|
+
:param TransfMatrix transform: The initial transformation matrix.
|
723
|
+
:param float width: The width of the lines.
|
724
|
+
:param float length: The length of the axes.
|
725
|
+
:param bool antialias: Whether to use antialiasing
|
726
|
+
"""
|
727
|
+
# Create GLLinePlotItems for the three axes.
|
728
|
+
# The initial positions are placeholders; they will be set properly in setData().
|
729
|
+
self.x_axis = gl.GLLinePlotItem(pos=np.zeros((2, 3)),
|
730
|
+
color=(1, 0, 0, 0.5),
|
731
|
+
glOptions='translucent',
|
732
|
+
width=width,
|
733
|
+
antialias=antialias)
|
734
|
+
self.y_axis = gl.GLLinePlotItem(pos=np.zeros((2, 3)),
|
735
|
+
color=(0, 1, 0, 0.5),
|
736
|
+
glOptions='translucent',
|
737
|
+
width=width,
|
738
|
+
antialias=antialias)
|
739
|
+
self.z_axis = gl.GLLinePlotItem(pos=np.zeros((2, 3)),
|
740
|
+
color=(0, 0, 1, 0.5),
|
741
|
+
glOptions='translucent',
|
742
|
+
width=width,
|
743
|
+
antialias=antialias)
|
744
|
+
|
745
|
+
# Set the initial transformation
|
746
|
+
self.tr = transform
|
747
|
+
self.length = length
|
748
|
+
self.setData(transform)
|
749
|
+
|
750
|
+
def setData(self, transform: TransfMatrix):
|
751
|
+
"""
|
752
|
+
Update the coordinate frame using a new 4x4 transformation matrix.
|
753
|
+
|
754
|
+
:param TransfMatrix transform: The new transformation matrix.
|
755
|
+
"""
|
756
|
+
self.tr = transform
|
757
|
+
|
758
|
+
# Update the positions for each axis.
|
759
|
+
self.x_axis.setData(pos=np.array([transform.t, transform.t + self.length * transform.n]))
|
760
|
+
self.y_axis.setData(pos=np.array([transform.t, transform.t + self.length * transform.o]))
|
761
|
+
self.z_axis.setData(pos=np.array([transform.t, transform.t + self.length * transform.a]))
|
762
|
+
|
763
|
+
def addToView(self, view: gl.GLViewWidget):
|
764
|
+
"""
|
765
|
+
Add all three axes to a GLViewWidget.
|
766
|
+
|
767
|
+
:param gl.GLViewWidget view: The view to add the axes to.
|
768
|
+
"""
|
769
|
+
view.addItem(self.x_axis)
|
770
|
+
view.addItem(self.y_axis)
|
771
|
+
view.addItem(self.z_axis)
|
772
|
+
else:
|
773
|
+
FramePlotHelper = None
|
774
|
+
|
775
|
+
if QtWidgets is not None:
|
776
|
+
class InteractivePlotterWidget(QtWidgets.QWidget):
|
777
|
+
"""
|
778
|
+
A QWidget that contains a PlotterPyqtgraph 3D view and interactive controls.
|
779
|
+
|
780
|
+
Containts (sliders and text boxes) for plotting and manipulating a mechanism.
|
781
|
+
"""
|
782
|
+
def __init__(self,
|
783
|
+
mechanism: RationalMechanism,
|
784
|
+
base=None,
|
785
|
+
show_tool: bool = True,
|
786
|
+
steps: int = 1000,
|
787
|
+
joint_sliders_lim: float = 1.0,
|
788
|
+
arrows_length: float = 1.0,
|
789
|
+
white_background: bool = False,
|
790
|
+
parent=None,
|
791
|
+
parent_app=None):
|
792
|
+
super().__init__(parent)
|
793
|
+
self.setMinimumSize(800, 600)
|
794
|
+
|
795
|
+
self.mechanism = mechanism
|
796
|
+
self.show_tool = show_tool
|
797
|
+
self.steps = steps
|
798
|
+
self.joint_sliders_lim = joint_sliders_lim
|
799
|
+
self.arrows_length = arrows_length
|
800
|
+
|
801
|
+
if base is not None:
|
802
|
+
if isinstance(base, TransfMatrix):
|
803
|
+
if not base.is_rotation():
|
804
|
+
raise ValueError("Given matrix is not proper rotation.")
|
805
|
+
self.base = base
|
806
|
+
self.base_arr = self.base.array()
|
807
|
+
elif isinstance(base, DualQuaternion):
|
808
|
+
self.base = TransfMatrix(base.dq2matrix())
|
809
|
+
self.base_arr = self.base.array()
|
810
|
+
else:
|
811
|
+
raise TypeError("Base must be a TransfMatrix or DualQuaternion instance.")
|
908
812
|
else:
|
909
|
-
|
910
|
-
|
911
|
-
glOptions=self.render_mode,
|
912
|
-
width=5,
|
913
|
-
antialias=True)
|
914
|
-
self.lines.append(line_item)
|
915
|
-
self.plotter.widget.addItem(line_item)
|
916
|
-
|
917
|
-
# --- If desired, initialize tool plot and tool frame ---
|
918
|
-
if self.show_tool:
|
919
|
-
self.tool_link = gl.GLLinePlotItem(pos=np.zeros((3, 3)),
|
920
|
-
color=(0, 1, 0, 0.5),
|
921
|
-
glOptions=self.render_mode,
|
922
|
-
width=5,
|
923
|
-
antialias=True)
|
924
|
-
self.plotter.widget.addItem(self.tool_link)
|
925
|
-
self.tool_frame = FramePlotHelper(
|
926
|
-
transform=TransfMatrix(self.mechanism.tool_frame.dq2matrix()),
|
927
|
-
length=self.arrows_length)
|
928
|
-
self.tool_frame.addToView(self.plotter.widget)
|
929
|
-
|
930
|
-
# --- Plot the tool path ---
|
931
|
-
self._plot_tool_path()
|
932
|
-
|
933
|
-
# --- Connect signals to slots ---
|
934
|
-
self.move_slider.valueChanged.connect(self.on_move_slider_changed)
|
935
|
-
self.text_box_angle.returnPressed.connect(self.on_angle_text_entered)
|
936
|
-
self.text_box_param.returnPressed.connect(self.on_param_text_entered)
|
937
|
-
self.save_mech_pkl.returnPressed.connect(self.on_save_save_mech_pkl)
|
938
|
-
self.save_figure_box.returnPressed.connect(self.on_save_figure_box)
|
939
|
-
for slider in self.joint_sliders:
|
940
|
-
slider.valueChanged.connect(self.on_joint_slider_changed)
|
941
|
-
|
942
|
-
# Set initial configuration (home position)
|
943
|
-
self.move_slider.setValue(self.move_slider.minimum())
|
944
|
-
self.plot_slider_update(self.move_slider.value() / 100.0)
|
945
|
-
|
946
|
-
self.setWindowTitle('Rational Linkages')
|
947
|
-
|
948
|
-
# --- Helper to create a “float slider” (using integer scaling) ---
|
949
|
-
def create_float_slider(self, min_val, max_val, init_val,
|
950
|
-
orientation=QtCore.Qt.Orientation.Horizontal):
|
951
|
-
slider = QtWidgets.QSlider(orientation)
|
952
|
-
slider.setMinimum(int(min_val * 100))
|
953
|
-
slider.setMaximum(int(max_val * 100))
|
954
|
-
slider.setValue(int(init_val * 100))
|
955
|
-
slider.setTickPosition(QtWidgets.QSlider.TickPosition.TicksBelow)
|
956
|
-
slider.setTickInterval(10)
|
957
|
-
return slider
|
958
|
-
|
959
|
-
def _init_joint_sliders(self, idx, slider_limit):
|
960
|
-
"""
|
961
|
-
Create a pair of vertical sliders for joint connection parameters.
|
962
|
-
(The slider values are scaled by 100.)
|
963
|
-
"""
|
964
|
-
slider0 = self.create_float_slider(-slider_limit,
|
965
|
-
slider_limit,
|
966
|
-
0.0,
|
967
|
-
orientation=QtCore.Qt.Orientation.Vertical)
|
968
|
-
slider1 = self.create_float_slider(-slider_limit,
|
969
|
-
slider_limit,
|
970
|
-
0.0,
|
971
|
-
orientation=QtCore.Qt.Orientation.Vertical)
|
972
|
-
return slider0, slider1
|
973
|
-
|
974
|
-
def _plot_tool_path(self):
|
975
|
-
"""
|
976
|
-
Plot the tool path (as a continuous line) using a set of computed points.
|
977
|
-
"""
|
978
|
-
t_lin = np.linspace(0, 2 * np.pi, self.steps)
|
979
|
-
t_vals = [self.mechanism.factorizations[0].joint_angle_to_t_param(t)
|
980
|
-
for t in t_lin]
|
981
|
-
ee_points = [self.mechanism.factorizations[0].direct_kinematics_of_tool(
|
982
|
-
t, self.mechanism.tool_frame.dq2point_via_matrix())
|
983
|
-
for t in t_vals]
|
984
|
-
|
985
|
-
if self.base_arr is not None:
|
986
|
-
# transform the end-effector points by the base transformation
|
987
|
-
ee_points = [self.base_arr @ np.insert(p, 0, 1) for p in ee_points]
|
988
|
-
# normalize
|
989
|
-
ee_points = [p[1:4]/p[0] for p in ee_points]
|
813
|
+
self.base = None
|
814
|
+
self.base_arr = None
|
990
815
|
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
glOptions=self.render_mode,
|
995
|
-
width=2,
|
996
|
-
antialias=True)
|
997
|
-
self.plotter.widget.addItem(tool_path)
|
998
|
-
|
999
|
-
# --- Slots for interactive control events ---
|
1000
|
-
def on_move_slider_changed(self, value):
|
1001
|
-
"""
|
1002
|
-
Called when the driving joint angle slider is moved.
|
1003
|
-
"""
|
1004
|
-
angle = value / 100.0 # Convert back to a float value.
|
1005
|
-
self.plot_slider_update(angle)
|
1006
|
-
|
1007
|
-
def on_angle_text_entered(self):
|
1008
|
-
"""
|
1009
|
-
Called when the angle text box is submitted.
|
1010
|
-
"""
|
1011
|
-
try:
|
1012
|
-
val = float(self.text_box_angle.text())
|
1013
|
-
# Normalize angle to [0, 2*pi]
|
1014
|
-
if val >= 0:
|
1015
|
-
val = val % (2 * np.pi)
|
816
|
+
self.white_background = white_background
|
817
|
+
if self.white_background:
|
818
|
+
self.render_mode = 'translucent'
|
1016
819
|
else:
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
|
1042
|
-
|
1043
|
-
|
1044
|
-
|
1045
|
-
|
1046
|
-
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
820
|
+
self.render_mode = 'additive'
|
821
|
+
|
822
|
+
# Create the PlotterPyqtgraph instance.
|
823
|
+
self.plotter = PlotterPyqtgraph(base=None,
|
824
|
+
steps=self.steps,
|
825
|
+
arrows_length=self.arrows_length,
|
826
|
+
white_background=self.white_background,
|
827
|
+
parent_app=parent_app)
|
828
|
+
# Optionally adjust the camera.
|
829
|
+
self.plotter.widget.setCameraPosition(distance=10, azimuth=30, elevation=30)
|
830
|
+
|
831
|
+
# Main layout: split between the 3D view and a control panel.
|
832
|
+
main_layout = QtWidgets.QHBoxLayout(self)
|
833
|
+
|
834
|
+
# Add the 3D view (PlotterPyqtgraph’s widget) to the layout.
|
835
|
+
main_layout.addWidget(self.plotter.widget, stretch=5)
|
836
|
+
|
837
|
+
# Create the control panel (on the right).
|
838
|
+
control_panel = QtWidgets.QWidget()
|
839
|
+
control_layout = QtWidgets.QVBoxLayout(control_panel)
|
840
|
+
|
841
|
+
# --- Driving joint angle slider ---
|
842
|
+
control_layout.addWidget(QtWidgets.QLabel("Joint angle [rad]:"))
|
843
|
+
self.move_slider = self.create_float_slider(0.0, 2 * np.pi, 0.0,
|
844
|
+
orientation=QtCore.Qt.Orientation.Horizontal)
|
845
|
+
control_layout.addWidget(self.move_slider)
|
846
|
+
|
847
|
+
# --- Text boxes ---
|
848
|
+
self.text_box_angle = QtWidgets.QLineEdit()
|
849
|
+
self.text_box_angle.setPlaceholderText("Set angle [rad]:")
|
850
|
+
control_layout.addWidget(self.text_box_angle)
|
851
|
+
|
852
|
+
self.text_box_param = QtWidgets.QLineEdit()
|
853
|
+
self.text_box_param.setPlaceholderText("Set parameter t [-]:")
|
854
|
+
control_layout.addWidget(self.text_box_param)
|
855
|
+
|
856
|
+
self.save_mech_pkl = QtWidgets.QLineEdit()
|
857
|
+
self.save_mech_pkl.setPlaceholderText("Save mechanism PKL, filename:")
|
858
|
+
control_layout.addWidget(self.save_mech_pkl)
|
859
|
+
|
860
|
+
self.save_figure_box = QtWidgets.QLineEdit()
|
861
|
+
self.save_figure_box.setPlaceholderText("Save figure PNG, filename:")
|
862
|
+
control_layout.addWidget(self.save_figure_box)
|
863
|
+
|
864
|
+
# --- Joint connection sliders ---
|
865
|
+
joint_sliders_layout = QtWidgets.QHBoxLayout()
|
866
|
+
self.joint_sliders = []
|
867
|
+
|
868
|
+
# Initialize sliders for each joint
|
869
|
+
for i in range(self.mechanism.num_joints):
|
870
|
+
slider0, slider1 = self._init_joint_sliders(i, self.joint_sliders_lim)
|
871
|
+
self.joint_sliders.append(slider0)
|
872
|
+
self.joint_sliders.append(slider1)
|
873
|
+
|
874
|
+
# Arrange sliders vertically for each joint
|
875
|
+
joint_layout = QtWidgets.QVBoxLayout()
|
876
|
+
|
877
|
+
joint_layout.addWidget(QtWidgets.QLabel(f"j{i}cp0"))
|
878
|
+
joint_layout.addWidget(slider0)
|
879
|
+
joint_layout.addWidget(QtWidgets.QLabel(f"j{i}cp1"))
|
880
|
+
joint_layout.addWidget(slider1)
|
881
|
+
|
882
|
+
joint_sliders_layout.addLayout(joint_layout)
|
883
|
+
|
884
|
+
control_layout.addLayout(joint_sliders_layout)
|
885
|
+
|
886
|
+
# Set default values for the first factorization
|
887
|
+
for i in range(self.mechanism.factorizations[0].number_of_factors):
|
888
|
+
default_val0 = self.mechanism.factorizations[0].linkage[i].points_params[0]
|
889
|
+
default_val1 = self.mechanism.factorizations[0].linkage[i].points_params[1]
|
890
|
+
self.joint_sliders[2 * i].setValue(int(default_val0 * 100))
|
891
|
+
self.joint_sliders[2 * i + 1].setValue(int(default_val1 * 100))
|
892
|
+
|
893
|
+
# Set default values for the second factorization
|
894
|
+
offset = 2 * self.mechanism.factorizations[0].number_of_factors
|
895
|
+
for i in range(self.mechanism.factorizations[1].number_of_factors):
|
896
|
+
default_val0 = self.mechanism.factorizations[1].linkage[i].points_params[0]
|
897
|
+
default_val1 = self.mechanism.factorizations[1].linkage[i].points_params[1]
|
898
|
+
self.joint_sliders[offset + 2 * i].setValue(int(default_val0 * 100))
|
899
|
+
self.joint_sliders[offset + 2 * i + 1].setValue(int(default_val1 * 100))
|
900
|
+
|
901
|
+
main_layout.addWidget(control_panel, stretch=1)
|
902
|
+
|
903
|
+
# --- Initialize plot items for the mechanism links ---
|
904
|
+
self.lines = []
|
905
|
+
num_lines = self.mechanism.num_joints * 2
|
906
|
+
for i in range(num_lines):
|
907
|
+
# if i is even, make the link color green, and joints red
|
908
|
+
if i % 2 == 0:
|
909
|
+
line_item = gl.GLLinePlotItem(pos=np.zeros((2, 3)),
|
910
|
+
color=(0, 1, 0, 1),
|
911
|
+
glOptions=self.render_mode,
|
912
|
+
width=5,
|
913
|
+
antialias=True)
|
914
|
+
else:
|
915
|
+
line_item = gl.GLLinePlotItem(pos=np.zeros((2, 3)),
|
916
|
+
color=(1, 0, 0, 1),
|
917
|
+
glOptions=self.render_mode,
|
918
|
+
width=5,
|
919
|
+
antialias=True)
|
920
|
+
self.lines.append(line_item)
|
921
|
+
self.plotter.widget.addItem(line_item)
|
922
|
+
|
923
|
+
# --- If desired, initialize tool plot and tool frame ---
|
924
|
+
if self.show_tool:
|
925
|
+
self.tool_link = gl.GLLinePlotItem(pos=np.zeros((3, 3)),
|
926
|
+
color=(0, 1, 0, 0.5),
|
927
|
+
glOptions=self.render_mode,
|
928
|
+
width=5,
|
929
|
+
antialias=True)
|
930
|
+
self.plotter.widget.addItem(self.tool_link)
|
931
|
+
self.tool_frame = FramePlotHelper(
|
932
|
+
transform=TransfMatrix(self.mechanism.tool_frame.dq2matrix()),
|
933
|
+
length=self.arrows_length)
|
934
|
+
self.tool_frame.addToView(self.plotter.widget)
|
935
|
+
|
936
|
+
# --- Plot the tool path ---
|
937
|
+
self._plot_tool_path()
|
938
|
+
|
939
|
+
# --- Connect signals to slots ---
|
940
|
+
self.move_slider.valueChanged.connect(self.on_move_slider_changed)
|
941
|
+
self.text_box_angle.returnPressed.connect(self.on_angle_text_entered)
|
942
|
+
self.text_box_param.returnPressed.connect(self.on_param_text_entered)
|
943
|
+
self.save_mech_pkl.returnPressed.connect(self.on_save_save_mech_pkl)
|
944
|
+
self.save_figure_box.returnPressed.connect(self.on_save_figure_box)
|
945
|
+
for slider in self.joint_sliders:
|
946
|
+
slider.valueChanged.connect(self.on_joint_slider_changed)
|
947
|
+
|
948
|
+
# Set initial configuration (home position)
|
949
|
+
self.move_slider.setValue(self.move_slider.minimum())
|
950
|
+
self.plot_slider_update(self.move_slider.value() / 100.0)
|
951
|
+
|
952
|
+
self.setWindowTitle('Rational Linkages')
|
953
|
+
|
954
|
+
# --- Helper to create a “float slider” (using integer scaling) ---
|
955
|
+
def create_float_slider(self, min_val, max_val, init_val,
|
956
|
+
orientation=QtCore.Qt.Orientation.Horizontal):
|
957
|
+
slider = QtWidgets.QSlider(orientation)
|
958
|
+
slider.setMinimum(int(min_val * 100))
|
959
|
+
slider.setMaximum(int(max_val * 100))
|
960
|
+
slider.setValue(int(init_val * 100))
|
961
|
+
slider.setTickPosition(QtWidgets.QSlider.TickPosition.TicksBelow)
|
962
|
+
slider.setTickInterval(10)
|
963
|
+
return slider
|
964
|
+
|
965
|
+
def _init_joint_sliders(self, idx, slider_limit):
|
966
|
+
"""
|
967
|
+
Create a pair of vertical sliders for joint connection parameters.
|
968
|
+
(The slider values are scaled by 100.)
|
969
|
+
"""
|
970
|
+
slider0 = self.create_float_slider(-slider_limit,
|
971
|
+
slider_limit,
|
972
|
+
0.0,
|
973
|
+
orientation=QtCore.Qt.Orientation.Vertical)
|
974
|
+
slider1 = self.create_float_slider(-slider_limit,
|
975
|
+
slider_limit,
|
976
|
+
0.0,
|
977
|
+
orientation=QtCore.Qt.Orientation.Vertical)
|
978
|
+
return slider0, slider1
|
979
|
+
|
980
|
+
def _plot_tool_path(self):
|
981
|
+
"""
|
982
|
+
Plot the tool path (as a continuous line) using a set of computed points.
|
983
|
+
"""
|
984
|
+
t_lin = np.linspace(0, 2 * np.pi, self.steps)
|
985
|
+
t_vals = [self.mechanism.factorizations[0].joint_angle_to_t_param(t)
|
986
|
+
for t in t_lin]
|
987
|
+
ee_points = [self.mechanism.factorizations[0].direct_kinematics_of_tool(
|
988
|
+
t, self.mechanism.tool_frame.dq2point_via_matrix())
|
989
|
+
for t in t_vals]
|
990
|
+
|
991
|
+
if self.base_arr is not None:
|
992
|
+
# transform the end-effector points by the base transformation
|
993
|
+
ee_points = [self.base_arr @ np.insert(p, 0, 1) for p in ee_points]
|
994
|
+
# normalize
|
995
|
+
ee_points = [p[1:4]/p[0] for p in ee_points]
|
996
|
+
|
997
|
+
pts = np.array(ee_points)
|
998
|
+
tool_path = gl.GLLinePlotItem(pos=pts,
|
999
|
+
color=(0.5, 0.5, 0.5, 1),
|
1000
|
+
glOptions=self.render_mode,
|
1001
|
+
width=2,
|
1002
|
+
antialias=True)
|
1003
|
+
self.plotter.widget.addItem(tool_path)
|
1004
|
+
|
1005
|
+
# --- Slots for interactive control events ---
|
1006
|
+
def on_move_slider_changed(self, value):
|
1007
|
+
"""
|
1008
|
+
Called when the driving joint angle slider is moved.
|
1009
|
+
"""
|
1010
|
+
angle = value / 100.0 # Convert back to a float value.
|
1011
|
+
self.plot_slider_update(angle)
|
1012
|
+
|
1013
|
+
def on_angle_text_entered(self):
|
1014
|
+
"""
|
1015
|
+
Called when the angle text box is submitted.
|
1016
|
+
"""
|
1017
|
+
try:
|
1018
|
+
val = float(self.text_box_angle.text())
|
1019
|
+
# Normalize angle to [0, 2*pi]
|
1020
|
+
if val >= 0:
|
1021
|
+
val = val % (2 * np.pi)
|
1022
|
+
else:
|
1023
|
+
val = (val % (2 * np.pi)) - np.pi
|
1024
|
+
self.move_slider.setValue(int(val * 100))
|
1025
|
+
except ValueError:
|
1026
|
+
pass
|
1027
|
+
|
1028
|
+
def on_param_text_entered(self):
|
1029
|
+
"""
|
1030
|
+
Called when the t-parameter text box is submitted.
|
1031
|
+
"""
|
1032
|
+
try:
|
1033
|
+
val = float(self.text_box_param.text())
|
1034
|
+
self.plot_slider_update(val, t_param=val)
|
1035
|
+
joint_angle = self.mechanism.factorizations[0].t_param_to_joint_angle(val)
|
1036
|
+
self.move_slider.setValue(int(joint_angle * 100))
|
1037
|
+
except ValueError:
|
1038
|
+
pass
|
1039
|
+
|
1040
|
+
def on_save_save_mech_pkl(self):
|
1041
|
+
"""
|
1042
|
+
Called when the save text box is submitted.
|
1043
|
+
"""
|
1044
|
+
filename = self.save_mech_pkl.text()
|
1045
|
+
self.mechanism.save(filename=filename)
|
1046
|
+
|
1047
|
+
QtWidgets.QMessageBox.information(self,
|
1048
|
+
"Success",
|
1049
|
+
f"Mechanism saved as {filename}.pkl")
|
1050
|
+
|
1051
|
+
def on_save_figure_box(self):
|
1052
|
+
"""
|
1053
|
+
Called when the filesave text box is submitted.
|
1054
|
+
|
1055
|
+
Saves the current figure in the specified format.
|
1056
|
+
"""
|
1057
|
+
filename = self.save_figure_box.text()
|
1058
|
+
|
1059
|
+
# better quality but does not save the text overlay
|
1060
|
+
#self.plotter.widget.readQImage().save(filename + "_old.png")
|
1061
|
+
#self.plotter.widget.readQImage().save(filename + "_old.png", quality=100)
|
1062
|
+
|
1063
|
+
image = QtGui.QImage(self.plotter.widget.size(),
|
1064
|
+
QtGui.QImage.Format.Format_ARGB32_Premultiplied)
|
1065
|
+
image.fill(QtCore.Qt.GlobalColor.transparent)
|
1066
|
+
|
1067
|
+
# Create a painter and render the widget into the image
|
1068
|
+
painter = QtGui.QPainter(image)
|
1069
|
+
self.plotter.widget.render(painter)
|
1070
|
+
painter.end()
|
1071
|
+
|
1072
|
+
# Save the image
|
1073
|
+
image.save(filename + ".png", "PNG", 80)
|
1074
|
+
|
1075
|
+
QtWidgets.QMessageBox.information(self,
|
1076
|
+
"Success",
|
1077
|
+
f"Figure saved as {filename}.png")
|
1078
|
+
|
1079
|
+
def on_joint_slider_changed(self, value):
|
1080
|
+
"""
|
1081
|
+
Called when any joint slider is changed.
|
1082
|
+
Updates the joint connection parameters and refreshes the plot.
|
1083
|
+
"""
|
1084
|
+
num_of_factors = self.mechanism.factorizations[0].number_of_factors
|
1085
|
+
# Update first factorization's linkage parameters.
|
1086
|
+
for i in range(num_of_factors):
|
1087
|
+
self.mechanism.factorizations[0].linkage[i].set_point_by_param(
|
1088
|
+
0, self.joint_sliders[2 * i].value() / 100.0)
|
1089
|
+
self.mechanism.factorizations[0].linkage[i].set_point_by_param(
|
1090
|
+
1, self.joint_sliders[2 * i + 1].value() / 100.0)
|
1091
|
+
# Update second factorization's linkage parameters.
|
1092
|
+
for i in range(num_of_factors):
|
1093
|
+
self.mechanism.factorizations[1].linkage[i].set_point_by_param(
|
1094
|
+
0, self.joint_sliders[2 * num_of_factors + 2 * i].value() / 100.0)
|
1095
|
+
self.mechanism.factorizations[1].linkage[i].set_point_by_param(
|
1096
|
+
1, self.joint_sliders[2 * num_of_factors + 1 + 2 * i].value() / 100.0)
|
1097
|
+
self.plot_slider_update(self.move_slider.value() / 100.0)
|
1098
|
+
|
1099
|
+
def plot_slider_update(self, angle, t_param=None):
|
1100
|
+
"""
|
1101
|
+
Update the mechanism plot based on the current joint angle or t parameter.
|
1102
|
+
"""
|
1103
|
+
if t_param is not None:
|
1104
|
+
t = t_param
|
1105
|
+
else:
|
1106
|
+
t = self.mechanism.factorizations[0].joint_angle_to_t_param(angle)
|
1068
1107
|
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
def on_joint_slider_changed(self, value):
|
1074
|
-
"""
|
1075
|
-
Called when any joint slider is changed.
|
1076
|
-
Updates the joint connection parameters and refreshes the plot.
|
1077
|
-
"""
|
1078
|
-
num_of_factors = self.mechanism.factorizations[0].number_of_factors
|
1079
|
-
# Update first factorization's linkage parameters.
|
1080
|
-
for i in range(num_of_factors):
|
1081
|
-
self.mechanism.factorizations[0].linkage[i].set_point_by_param(
|
1082
|
-
0, self.joint_sliders[2 * i].value() / 100.0)
|
1083
|
-
self.mechanism.factorizations[0].linkage[i].set_point_by_param(
|
1084
|
-
1, self.joint_sliders[2 * i + 1].value() / 100.0)
|
1085
|
-
# Update second factorization's linkage parameters.
|
1086
|
-
for i in range(num_of_factors):
|
1087
|
-
self.mechanism.factorizations[1].linkage[i].set_point_by_param(
|
1088
|
-
0, self.joint_sliders[2 * num_of_factors + 2 * i].value() / 100.0)
|
1089
|
-
self.mechanism.factorizations[1].linkage[i].set_point_by_param(
|
1090
|
-
1, self.joint_sliders[2 * num_of_factors + 1 + 2 * i].value() / 100.0)
|
1091
|
-
self.plot_slider_update(self.move_slider.value() / 100.0)
|
1092
|
-
|
1093
|
-
def plot_slider_update(self, angle, t_param=None):
|
1094
|
-
"""
|
1095
|
-
Update the mechanism plot based on the current joint angle or t parameter.
|
1096
|
-
"""
|
1097
|
-
if t_param is not None:
|
1098
|
-
t = t_param
|
1099
|
-
else:
|
1100
|
-
t = self.mechanism.factorizations[0].joint_angle_to_t_param(angle)
|
1101
|
-
|
1102
|
-
# Compute link positions.
|
1103
|
-
links = (self.mechanism.factorizations[0].direct_kinematics(t) +
|
1104
|
-
self.mechanism.factorizations[1].direct_kinematics(t)[::-1])
|
1105
|
-
links.insert(0, links[-1])
|
1106
|
-
|
1107
|
-
if self.base is not None:
|
1108
|
-
# Transform the links by the base transformation.
|
1109
|
-
links = [self.base_arr @ np.insert(p, 0, 1) for p in links]
|
1110
|
-
# Normalize the homogeneous coordinates.
|
1111
|
-
links = [p[1:4] / p[0] for p in links]
|
1112
|
-
|
1113
|
-
# Update each line segment.
|
1114
|
-
for i, line in enumerate(self.lines):
|
1115
|
-
pt1 = links[i]
|
1116
|
-
pt2 = links[i+1]
|
1117
|
-
pts = np.array([pt1, pt2])
|
1118
|
-
line.setData(pos=pts)
|
1119
|
-
|
1120
|
-
if self.show_tool:
|
1121
|
-
pts0 = self.mechanism.factorizations[0].direct_kinematics(t)[-1]
|
1122
|
-
pts1 = self.mechanism.factorizations[0].direct_kinematics_of_tool(
|
1123
|
-
t, self.mechanism.tool_frame.dq2point_via_matrix())
|
1124
|
-
pts2 = self.mechanism.factorizations[1].direct_kinematics(t)[-1]
|
1125
|
-
|
1126
|
-
tool_triangle = [pts0, pts1, pts2]
|
1108
|
+
# Compute link positions.
|
1109
|
+
links = (self.mechanism.factorizations[0].direct_kinematics(t) +
|
1110
|
+
self.mechanism.factorizations[1].direct_kinematics(t)[::-1])
|
1111
|
+
links.insert(0, links[-1])
|
1127
1112
|
|
1128
1113
|
if self.base is not None:
|
1129
|
-
# Transform the
|
1130
|
-
|
1131
|
-
for p in tool_triangle]
|
1114
|
+
# Transform the links by the base transformation.
|
1115
|
+
links = [self.base_arr @ np.insert(p, 0, 1) for p in links]
|
1132
1116
|
# Normalize the homogeneous coordinates.
|
1133
|
-
|
1134
|
-
|
1135
|
-
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1149
|
-
|
1150
|
-
|
1117
|
+
links = [p[1:4] / p[0] for p in links]
|
1118
|
+
|
1119
|
+
# Update each line segment.
|
1120
|
+
for i, line in enumerate(self.lines):
|
1121
|
+
pt1 = links[i]
|
1122
|
+
pt2 = links[i+1]
|
1123
|
+
pts = np.array([pt1, pt2])
|
1124
|
+
line.setData(pos=pts)
|
1125
|
+
|
1126
|
+
if self.show_tool:
|
1127
|
+
pts0 = self.mechanism.factorizations[0].direct_kinematics(t)[-1]
|
1128
|
+
pts1 = self.mechanism.factorizations[0].direct_kinematics_of_tool(
|
1129
|
+
t, self.mechanism.tool_frame.dq2point_via_matrix())
|
1130
|
+
pts2 = self.mechanism.factorizations[1].direct_kinematics(t)[-1]
|
1131
|
+
|
1132
|
+
tool_triangle = [pts0, pts1, pts2]
|
1133
|
+
|
1134
|
+
if self.base is not None:
|
1135
|
+
# Transform the tool triangle by the base transformation.
|
1136
|
+
tool_triangle = [self.base_arr @ np.insert(p, 0, 1)
|
1137
|
+
for p in tool_triangle]
|
1138
|
+
# Normalize the homogeneous coordinates.
|
1139
|
+
tool_triangle = [p[1:4] / p[0] for p in tool_triangle]
|
1140
|
+
|
1141
|
+
self.tool_link.setData(pos=np.array(tool_triangle))
|
1142
|
+
|
1143
|
+
# Update tool frame (pose) arrows.
|
1144
|
+
pose_dq = DualQuaternion(self.mechanism.evaluate(t))
|
1145
|
+
# Compute the pose matrix by composing the mechanism’s pose and tool frame.
|
1146
|
+
pose_matrix = TransfMatrix(pose_dq.dq2matrix()) * TransfMatrix(
|
1147
|
+
self.mechanism.tool_frame.dq2matrix())
|
1148
|
+
|
1149
|
+
if self.base is not None:
|
1150
|
+
# Transform the pose matrix by the base transformation.
|
1151
|
+
pose_matrix = self.base * pose_matrix
|
1152
|
+
|
1153
|
+
self.tool_frame.setData(pose_matrix)
|
1154
|
+
|
1155
|
+
self.plotter.widget.update()
|
1156
|
+
else:
|
1157
|
+
InteractivePlotterWidget = None
|
1151
1158
|
|
1152
1159
|
class InteractivePlotter:
|
1153
1160
|
"""
|