rational-linkages 2.0.0__cp310-cp310-macosx_12_0_arm64.whl → 2.2.3__cp310-cp310-macosx_12_0_arm64.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.
Files changed (31) hide show
  1. rational_linkages/CollisionAnalyser.py +323 -21
  2. rational_linkages/CollisionFreeOptimization.py +8 -4
  3. rational_linkages/DualQuaternion.py +5 -2
  4. rational_linkages/ExudynAnalysis.py +2 -1
  5. rational_linkages/FactorizationProvider.py +6 -5
  6. rational_linkages/MiniBall.py +9 -2
  7. rational_linkages/MotionApproximation.py +7 -3
  8. rational_linkages/MotionDesigner.py +553 -540
  9. rational_linkages/MotionFactorization.py +6 -5
  10. rational_linkages/MotionInterpolation.py +7 -7
  11. rational_linkages/NormalizedLine.py +1 -1
  12. rational_linkages/NormalizedPlane.py +1 -1
  13. rational_linkages/Plotter.py +1 -1
  14. rational_linkages/PlotterMatplotlib.py +27 -13
  15. rational_linkages/PlotterPyqtgraph.py +596 -534
  16. rational_linkages/PointHomogeneous.py +6 -3
  17. rational_linkages/RationalBezier.py +64 -4
  18. rational_linkages/RationalCurve.py +13 -5
  19. rational_linkages/RationalDualQuaternion.py +5 -4
  20. rational_linkages/RationalMechanism.py +48 -33
  21. rational_linkages/SingularityAnalysis.py +4 -5
  22. rational_linkages/StaticMechanism.py +4 -5
  23. rational_linkages/__init__.py +3 -2
  24. rational_linkages/utils.py +60 -3
  25. rational_linkages/utils_rust.cpython-310-darwin.so +0 -0
  26. {rational_linkages-2.0.0.dist-info → rational_linkages-2.2.3.dist-info}/METADATA +32 -18
  27. rational_linkages-2.2.3.dist-info/RECORD +40 -0
  28. rational_linkages-2.0.0.dist-info/RECORD +0 -40
  29. {rational_linkages-2.0.0.dist-info → rational_linkages-2.2.3.dist-info}/WHEEL +0 -0
  30. {rational_linkages-2.0.0.dist-info → rational_linkages-2.2.3.dist-info}/licenses/LICENSE +0 -0
  31. {rational_linkages-2.0.0.dist-info → rational_linkages-2.2.3.dist-info}/top_level.txt +0 -0
@@ -1,21 +1,34 @@
1
1
  import sys
2
2
  import numpy as np
3
3
 
4
- from PyQt6.QtWidgets import QApplication
5
- from PyQt6 import QtCore, QtGui, QtWidgets
6
- import pyqtgraph.opengl as gl
4
+ from warnings import warn
7
5
 
8
6
  from .DualQuaternion import DualQuaternion
7
+ from .Linkage import LineSegment
8
+ from .MiniBall import MiniBall
9
9
  from .MotionFactorization import MotionFactorization
10
10
  from .NormalizedLine import NormalizedLine
11
11
  from .NormalizedPlane import NormalizedPlane
12
12
  from .PointHomogeneous import PointHomogeneous, PointOrbit
13
- from .RationalBezier import RationalBezier
13
+ from .RationalBezier import RationalBezier, RationalSoo
14
14
  from .RationalCurve import RationalCurve
15
15
  from .RationalMechanism import RationalMechanism
16
16
  from .TransfMatrix import TransfMatrix
17
- from .MiniBall import MiniBall
18
- from .Linkage import LineSegment
17
+
18
+ # Try importing GUI components
19
+ try:
20
+ import pyqtgraph.opengl as gl
21
+ from PyQt6 import QtCore, QtGui, QtWidgets
22
+ from PyQt6.QtWidgets import QApplication
23
+ except (ImportError, OSError):
24
+ warn("Failed to import OpenGL or PyQt6. If you expect interactive GUI to work, "
25
+ "please check the package installation.")
26
+
27
+ gl = None
28
+ QtCore = None
29
+ QtGui = None
30
+ QtWidgets = None
31
+ QApplication = None
19
32
 
20
33
 
21
34
  class PlotterPyqtgraph:
@@ -142,6 +155,8 @@ class PlotterPyqtgraph:
142
155
  self._plot_dual_quaternion(object_to_plot, **kwargs)
143
156
  elif type_to_plot == "is_transf_matrix":
144
157
  self._plot_transf_matrix(object_to_plot, **kwargs)
158
+ elif type_to_plot == "is_gauss_legendre":
159
+ self._plot_gauss_legendre(object_to_plot, **kwargs)
145
160
  elif type_to_plot == "is_rational_curve":
146
161
  self._plot_rational_curve(object_to_plot, **kwargs)
147
162
  elif type_to_plot == "is_rational_bezier":
@@ -171,6 +186,8 @@ class PlotterPyqtgraph:
171
186
  return "is_line"
172
187
  elif isinstance(object_to_plot, PointHomogeneous):
173
188
  return "is_point"
189
+ elif isinstance(object_to_plot, RationalSoo):
190
+ return "is_gauss_legendre"
174
191
  elif isinstance(object_to_plot, RationalBezier):
175
192
  return "is_rational_bezier"
176
193
  elif isinstance(object_to_plot, RationalCurve):
@@ -414,6 +431,44 @@ class PlotterPyqtgraph:
414
431
  antialias=True)
415
432
  self.widget.addItem(cp_line)
416
433
 
434
+ def _plot_gauss_legendre(self,
435
+ curve: RationalSoo,
436
+ plot_control_points: bool = True,
437
+ **kwargs):
438
+ """
439
+ Plot a Gauss-Legendre rational curve along with its control points.
440
+
441
+ Similar to plot Bezier, but specifically for Gauss-Legendre curves.
442
+
443
+ :param RationalSoo curve: The Gauss-Legendre curve to plot.
444
+ :param bool plot_control_points: Whether to plot the control points.
445
+ :param kwargs: Additional keyword arguments for customization.
446
+ """
447
+ interval = kwargs.pop('interval', (-1, 1))
448
+ x, y, z, x_cp, y_cp, z_cp = curve.get_plot_data(interval, self.steps)
449
+
450
+ pts = np.column_stack((x, y, z))
451
+ color = self._get_color(kwargs.get('color', 'yellow'), (1, 0, 1, 1))
452
+ line_item = gl.GLLinePlotItem(pos=pts,
453
+ color=color,
454
+ glOptions=self.render_mode,
455
+ width=2,
456
+ antialias=True)
457
+ self.widget.addItem(line_item)
458
+ if plot_control_points:
459
+ cp = np.column_stack((x_cp, y_cp, z_cp))
460
+ scatter = gl.GLScatterPlotItem(pos=cp,
461
+ color=(1, 0, 0, 1),
462
+ glOptions=self.render_mode,
463
+ size=8)
464
+ self.widget.addItem(scatter)
465
+ cp_line = gl.GLLinePlotItem(pos=cp,
466
+ color=(1, 0, 0, 1),
467
+ glOptions=self.render_mode,
468
+ width=1,
469
+ antialias=True)
470
+ self.widget.addItem(cp_line)
471
+
417
472
  def _plot_motion_factorization(self, factorization: MotionFactorization, **kwargs):
418
473
  """
419
474
  Plot the motion factorization as a 3D line.
@@ -556,543 +611,550 @@ class PlotterPyqtgraph:
556
611
  event.accept()
557
612
 
558
613
 
559
- class CustomGLViewWidget(gl.GLViewWidget):
560
- def __init__(self, white_background=False, *args, **kwargs):
561
- super().__init__(*args, **kwargs)
562
- self.labels = []
563
- self.white_background = white_background
564
- # Create an overlay widget for displaying text
565
- self.text_overlay = QtWidgets.QWidget(self)
566
- self.text_overlay.setAttribute(
567
- QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents)
568
- self.text_overlay.setStyleSheet("background:transparent;")
569
- self.text_overlay.resize(self.size())
570
- self.text_overlay.show()
571
-
572
- def resizeEvent(self, event):
573
- super().resizeEvent(event)
574
- 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;")
575
625
  self.text_overlay.resize(self.size())
576
-
577
- def add_label(self, point, text):
578
- """Adds a label for a 3D point."""
579
- self.labels.append({'point': point, 'text': text})
580
- self.update()
581
-
582
- def paintEvent(self, event):
583
- # Only handle standard OpenGL rendering here - no mixing with QPainter
584
- super().paintEvent(event)
585
-
586
- # Schedule label painting as a separate operation
587
- QtCore.QTimer.singleShot(0, self.update_text_overlay)
588
-
589
- def update_text_overlay(self):
590
- """Update the text overlay with current labels"""
591
- # Create a new painter for the overlay widget
592
- self.text_overlay.update()
593
-
594
- def _obtain_label_vec(self, pt):
595
- """Obtain the label vector."""
596
- # Convert the 3D point to homogeneous coordinates
597
- if isinstance(pt, np.ndarray):
598
- point_vec = pt
599
- elif isinstance(pt, PointHomogeneous):
600
- point_vec = [pt.coordinates_normalized[1],
601
- pt.coordinates_normalized[2],
602
- pt.coordinates_normalized[3]]
603
- elif isinstance(pt, TransfMatrix):
604
- point_vec = [pt.t[0], pt.t[1], pt.t[2]]
605
- elif isinstance(pt, FramePlotHelper):
606
- point_vec = [pt.tr.t[0], pt.tr.t[1], pt.tr.t[2]]
607
- else: # is pyqtgraph marker (scatter)
608
- point_vec = [pt.pos[0][0], pt.pos[0][1], pt.pos[0][2]]
609
-
610
- return QtGui.QVector4D(point_vec[0], point_vec[1], point_vec[2], 1.0)
611
-
612
- # This method renders text on the overlay
613
- def paintOverlay(self, event):
614
- painter = QtGui.QPainter(self.text_overlay)
615
- painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
616
- if self.white_background:
617
- painter.setPen(QtGui.QColor(QtCore.Qt.GlobalColor.black))
618
- else:
619
- painter.setPen(QtGui.QColor(QtCore.Qt.GlobalColor.white))
620
-
621
- # Get the Model-View-Projection matrix
622
- projection_matrix = self.projectionMatrix()
623
- view_matrix = self.viewMatrix()
624
- mvp = projection_matrix * view_matrix
625
-
626
- # Draw all labels
627
- for entry in self.labels:
628
- point = entry['point']
629
- text = entry['text']
630
-
631
- projected = mvp.map(self._obtain_label_vec(point))
632
- if projected.w() != 0:
633
- ndc_x = projected.x() / projected.w()
634
- ndc_y = projected.y() / projected.w()
635
- # Check if the point is in front of the camera
636
- if projected.z() / projected.w() < 1.0:
637
- x = int((ndc_x * 0.5 + 0.5) * self.width())
638
- y = int((1 - (ndc_y * 0.5 + 0.5)) * self.height())
639
- painter.drawText(x, y, text)
640
-
641
- painter.end()
642
-
643
- def showEvent(self, event):
644
- super().showEvent(event)
645
- self.text_overlay.installEventFilter(self)
646
-
647
- def eventFilter(self, obj, event):
648
- if obj is self.text_overlay and event.type() == QtCore.QEvent.Type.Paint:
649
- self.paintOverlay(event)
650
- return True
651
- return super().eventFilter(obj, event)
652
-
653
-
654
- class FramePlotHelper:
655
- def __init__(self,
656
- transform: TransfMatrix = TransfMatrix(),
657
- width: float = 2.,
658
- length: float = 1.,
659
- antialias: bool = True):
660
- """
661
- Create a coordinate frame using three GLLinePlotItems.
662
-
663
- :param TransfMatrix transform: The initial transformation matrix.
664
- :param float width: The width of the lines.
665
- :param float length: The length of the axes.
666
- :param bool antialias: Whether to use antialiasing
667
- """
668
- # Create GLLinePlotItems for the three axes.
669
- # The initial positions are placeholders; they will be set properly in setData().
670
- self.x_axis = gl.GLLinePlotItem(pos=np.zeros((2, 3)),
671
- color=(1, 0, 0, 0.5),
672
- glOptions='translucent',
673
- width=width,
674
- antialias=antialias)
675
- self.y_axis = gl.GLLinePlotItem(pos=np.zeros((2, 3)),
676
- color=(0, 1, 0, 0.5),
677
- glOptions='translucent',
678
- width=width,
679
- antialias=antialias)
680
- self.z_axis = gl.GLLinePlotItem(pos=np.zeros((2, 3)),
681
- color=(0, 0, 1, 0.5),
682
- glOptions='translucent',
683
- width=width,
684
- antialias=antialias)
685
-
686
- # Set the initial transformation
687
- self.tr = transform
688
- self.length = length
689
- self.setData(transform)
690
-
691
- def setData(self, transform: TransfMatrix):
692
- """
693
- Update the coordinate frame using a new 4x4 transformation matrix.
694
-
695
- :param TransfMatrix transform: The new transformation matrix.
696
- """
697
- self.tr = transform
698
-
699
- # Update the positions for each axis.
700
- self.x_axis.setData(pos=np.array([transform.t, transform.t + self.length * transform.n]))
701
- self.y_axis.setData(pos=np.array([transform.t, transform.t + self.length * transform.o]))
702
- self.z_axis.setData(pos=np.array([transform.t, transform.t + self.length * transform.a]))
703
-
704
- def addToView(self, view: gl.GLViewWidget):
705
- """
706
- Add all three axes to a GLViewWidget.
707
-
708
- :param gl.GLViewWidget view: The view to add the axes to.
709
- """
710
- view.addItem(self.x_axis)
711
- view.addItem(self.y_axis)
712
- view.addItem(self.z_axis)
713
-
714
-
715
- class InteractivePlotterWidget(QtWidgets.QWidget):
716
- """
717
- A QWidget that contains a PlotterPyqtgraph 3D view and interactive controls.
718
-
719
- Containts (sliders and text boxes) for plotting and manipulating a mechanism.
720
- """
721
- def __init__(self,
722
- mechanism: RationalMechanism,
723
- base=None,
724
- show_tool: bool = True,
725
- steps: int = 1000,
726
- joint_sliders_lim: float = 1.0,
727
- arrows_length: float = 1.0,
728
- white_background: bool = False,
729
- parent=None,
730
- parent_app=None):
731
- super().__init__(parent)
732
- self.setMinimumSize(800, 600)
733
-
734
- self.mechanism = mechanism
735
- self.show_tool = show_tool
736
- self.steps = steps
737
- self.joint_sliders_lim = joint_sliders_lim
738
- self.arrows_length = arrows_length
739
-
740
- if base is not None:
741
- if isinstance(base, TransfMatrix):
742
- if not base.is_rotation():
743
- raise ValueError("Given matrix is not proper rotation.")
744
- self.base = base
745
- self.base_arr = self.base.array()
746
- elif isinstance(base, DualQuaternion):
747
- self.base = TransfMatrix(base.dq2matrix())
748
- 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))
749
674
  else:
750
- raise TypeError("Base must be a TransfMatrix or DualQuaternion instance.")
751
- else:
752
- self.base = None
753
- self.base_arr = None
754
-
755
- self.white_background = white_background
756
- if self.white_background:
757
- self.render_mode = 'translucent'
758
- else:
759
- self.render_mode = 'additive'
760
-
761
- # Create the PlotterPyqtgraph instance.
762
- self.plotter = PlotterPyqtgraph(base=None,
763
- steps=self.steps,
764
- arrows_length=self.arrows_length,
765
- white_background=self.white_background,
766
- parent_app=parent_app)
767
- # Optionally adjust the camera.
768
- self.plotter.widget.setCameraPosition(distance=10, azimuth=30, elevation=30)
769
-
770
- # Main layout: split between the 3D view and a control panel.
771
- main_layout = QtWidgets.QHBoxLayout(self)
772
-
773
- # Add the 3D view (PlotterPyqtgraph’s widget) to the layout.
774
- main_layout.addWidget(self.plotter.widget, stretch=5)
775
-
776
- # Create the control panel (on the right).
777
- control_panel = QtWidgets.QWidget()
778
- control_layout = QtWidgets.QVBoxLayout(control_panel)
779
-
780
- # --- Driving joint angle slider ---
781
- control_layout.addWidget(QtWidgets.QLabel("Joint angle [rad]:"))
782
- self.move_slider = self.create_float_slider(0.0, 2 * np.pi, 0.0,
783
- orientation=QtCore.Qt.Orientation.Horizontal)
784
- control_layout.addWidget(self.move_slider)
785
-
786
- # --- Text boxes ---
787
- self.text_box_angle = QtWidgets.QLineEdit()
788
- self.text_box_angle.setPlaceholderText("Set angle [rad]:")
789
- control_layout.addWidget(self.text_box_angle)
790
-
791
- self.text_box_param = QtWidgets.QLineEdit()
792
- self.text_box_param.setPlaceholderText("Set parameter t [-]:")
793
- control_layout.addWidget(self.text_box_param)
794
-
795
- self.save_mech_pkl = QtWidgets.QLineEdit()
796
- self.save_mech_pkl.setPlaceholderText("Save mechanism PKL, filename:")
797
- control_layout.addWidget(self.save_mech_pkl)
798
-
799
- self.save_figure_box = QtWidgets.QLineEdit()
800
- self.save_figure_box.setPlaceholderText("Save figure PNG, filename:")
801
- control_layout.addWidget(self.save_figure_box)
802
-
803
- # --- Joint connection sliders ---
804
- joint_sliders_layout = QtWidgets.QHBoxLayout()
805
- self.joint_sliders = []
806
-
807
- # Initialize sliders for each joint
808
- for i in range(self.mechanism.num_joints):
809
- slider0, slider1 = self._init_joint_sliders(i, self.joint_sliders_lim)
810
- self.joint_sliders.append(slider0)
811
- self.joint_sliders.append(slider1)
812
-
813
- # Arrange sliders vertically for each joint
814
- joint_layout = QtWidgets.QVBoxLayout()
815
-
816
- joint_layout.addWidget(QtWidgets.QLabel(f"j{i}cp0"))
817
- joint_layout.addWidget(slider0)
818
- joint_layout.addWidget(QtWidgets.QLabel(f"j{i}cp1"))
819
- joint_layout.addWidget(slider1)
820
-
821
- joint_sliders_layout.addLayout(joint_layout)
822
-
823
- control_layout.addLayout(joint_sliders_layout)
824
-
825
- # Set default values for the first factorization
826
- for i in range(self.mechanism.factorizations[0].number_of_factors):
827
- default_val0 = self.mechanism.factorizations[0].linkage[i].points_params[0]
828
- default_val1 = self.mechanism.factorizations[0].linkage[i].points_params[1]
829
- self.joint_sliders[2 * i].setValue(int(default_val0 * 100))
830
- self.joint_sliders[2 * i + 1].setValue(int(default_val1 * 100))
831
-
832
- # Set default values for the second factorization
833
- offset = 2 * self.mechanism.factorizations[0].number_of_factors
834
- for i in range(self.mechanism.factorizations[1].number_of_factors):
835
- default_val0 = self.mechanism.factorizations[1].linkage[i].points_params[0]
836
- default_val1 = self.mechanism.factorizations[1].linkage[i].points_params[1]
837
- self.joint_sliders[offset + 2 * i].setValue(int(default_val0 * 100))
838
- self.joint_sliders[offset + 2 * i + 1].setValue(int(default_val1 * 100))
839
-
840
- main_layout.addWidget(control_panel, stretch=1)
841
-
842
- # --- Initialize plot items for the mechanism links ---
843
- self.lines = []
844
- num_lines = self.mechanism.num_joints * 2
845
- for i in range(num_lines):
846
- # if i is even, make the link color green, and joints red
847
- if i % 2 == 0:
848
- line_item = gl.GLLinePlotItem(pos=np.zeros((2, 3)),
849
- color=(0, 1, 0, 1),
850
- glOptions=self.render_mode,
851
- width=5,
852
- antialias=True)
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.")
853
812
  else:
854
- line_item = gl.GLLinePlotItem(pos=np.zeros((2, 3)),
855
- color=(1, 0, 0, 1),
856
- glOptions=self.render_mode,
857
- width=5,
858
- antialias=True)
859
- self.lines.append(line_item)
860
- self.plotter.widget.addItem(line_item)
861
-
862
- # --- If desired, initialize tool plot and tool frame ---
863
- if self.show_tool:
864
- self.tool_link = gl.GLLinePlotItem(pos=np.zeros((3, 3)),
865
- color=(0, 1, 0, 0.5),
866
- glOptions=self.render_mode,
867
- width=5,
868
- antialias=True)
869
- self.plotter.widget.addItem(self.tool_link)
870
- self.tool_frame = FramePlotHelper(
871
- transform=TransfMatrix(self.mechanism.tool_frame.dq2matrix()),
872
- length=self.arrows_length)
873
- self.tool_frame.addToView(self.plotter.widget)
874
-
875
- # --- Plot the tool path ---
876
- self._plot_tool_path()
877
-
878
- # --- Connect signals to slots ---
879
- self.move_slider.valueChanged.connect(self.on_move_slider_changed)
880
- self.text_box_angle.returnPressed.connect(self.on_angle_text_entered)
881
- self.text_box_param.returnPressed.connect(self.on_param_text_entered)
882
- self.save_mech_pkl.returnPressed.connect(self.on_save_save_mech_pkl)
883
- self.save_figure_box.returnPressed.connect(self.on_save_figure_box)
884
- for slider in self.joint_sliders:
885
- slider.valueChanged.connect(self.on_joint_slider_changed)
886
-
887
- # Set initial configuration (home position)
888
- self.move_slider.setValue(self.move_slider.minimum())
889
- self.plot_slider_update(self.move_slider.value() / 100.0)
890
-
891
- self.setWindowTitle('Rational Linkages')
892
-
893
- # --- Helper to create a “float slider” (using integer scaling) ---
894
- def create_float_slider(self, min_val, max_val, init_val,
895
- orientation=QtCore.Qt.Orientation.Horizontal):
896
- slider = QtWidgets.QSlider(orientation)
897
- slider.setMinimum(int(min_val * 100))
898
- slider.setMaximum(int(max_val * 100))
899
- slider.setValue(int(init_val * 100))
900
- slider.setTickPosition(QtWidgets.QSlider.TickPosition.TicksBelow)
901
- slider.setTickInterval(10)
902
- return slider
903
-
904
- def _init_joint_sliders(self, idx, slider_limit):
905
- """
906
- Create a pair of vertical sliders for joint connection parameters.
907
- (The slider values are scaled by 100.)
908
- """
909
- slider0 = self.create_float_slider(-slider_limit,
910
- slider_limit,
911
- 0.0,
912
- orientation=QtCore.Qt.Orientation.Vertical)
913
- slider1 = self.create_float_slider(-slider_limit,
914
- slider_limit,
915
- 0.0,
916
- orientation=QtCore.Qt.Orientation.Vertical)
917
- return slider0, slider1
918
-
919
- def _plot_tool_path(self):
920
- """
921
- Plot the tool path (as a continuous line) using a set of computed points.
922
- """
923
- t_lin = np.linspace(0, 2 * np.pi, self.steps)
924
- t_vals = [self.mechanism.factorizations[0].joint_angle_to_t_param(t)
925
- for t in t_lin]
926
- ee_points = [self.mechanism.factorizations[0].direct_kinematics_of_tool(
927
- t, self.mechanism.tool_frame.dq2point_via_matrix())
928
- for t in t_vals]
929
-
930
- if self.base_arr is not None:
931
- # transform the end-effector points by the base transformation
932
- ee_points = [self.base_arr @ np.insert(p, 0, 1) for p in ee_points]
933
- # normalize
934
- ee_points = [p[1:4]/p[0] for p in ee_points]
935
-
936
- pts = np.array(ee_points)
937
- tool_path = gl.GLLinePlotItem(pos=pts,
938
- color=(0.5, 0.5, 0.5, 1),
939
- glOptions=self.render_mode,
940
- width=2,
941
- antialias=True)
942
- self.plotter.widget.addItem(tool_path)
943
-
944
- # --- Slots for interactive control events ---
945
- def on_move_slider_changed(self, value):
946
- """
947
- Called when the driving joint angle slider is moved.
948
- """
949
- angle = value / 100.0 # Convert back to a float value.
950
- self.plot_slider_update(angle)
813
+ self.base = None
814
+ self.base_arr = None
951
815
 
952
- def on_angle_text_entered(self):
953
- """
954
- Called when the angle text box is submitted.
955
- """
956
- try:
957
- val = float(self.text_box_angle.text())
958
- # Normalize angle to [0, 2*pi]
959
- if val >= 0:
960
- val = val % (2 * np.pi)
816
+ self.white_background = white_background
817
+ if self.white_background:
818
+ self.render_mode = 'translucent'
961
819
  else:
962
- val = (val % (2 * np.pi)) - np.pi
963
- self.move_slider.setValue(int(val * 100))
964
- except ValueError:
965
- pass
966
-
967
- def on_param_text_entered(self):
968
- """
969
- Called when the t-parameter text box is submitted.
970
- """
971
- try:
972
- val = float(self.text_box_param.text())
973
- self.plot_slider_update(val, t_param=val)
974
- joint_angle = self.mechanism.factorizations[0].t_param_to_joint_angle(val)
975
- self.move_slider.setValue(int(joint_angle * 100))
976
- except ValueError:
977
- pass
978
-
979
- def on_save_save_mech_pkl(self):
980
- """
981
- Called when the save text box is submitted.
982
- """
983
- filename = self.save_mech_pkl.text()
984
- self.mechanism.save(filename=filename)
985
-
986
- QtWidgets.QMessageBox.information(self,
987
- "Success",
988
- f"Mechanism saved as {filename}.pkl")
989
-
990
- def on_save_figure_box(self):
991
- """
992
- Called when the filesave text box is submitted.
993
-
994
- Saves the current figure in the specified format.
995
- """
996
- filename = self.save_figure_box.text()
997
-
998
- # better quality but does not save the text overlay
999
- #self.plotter.widget.readQImage().save(filename + "_old.png")
1000
- #self.plotter.widget.readQImage().save(filename + "_old.png", quality=100)
1001
-
1002
- image = QtGui.QImage(self.plotter.widget.size(),
1003
- QtGui.QImage.Format.Format_ARGB32_Premultiplied)
1004
- image.fill(QtCore.Qt.GlobalColor.transparent)
1005
-
1006
- # Create a painter and render the widget into the image
1007
- painter = QtGui.QPainter(image)
1008
- self.plotter.widget.render(painter)
1009
- painter.end()
1010
-
1011
- # Save the image
1012
- image.save(filename + ".png", "PNG", 80)
1013
-
1014
- QtWidgets.QMessageBox.information(self,
1015
- "Success",
1016
- f"Figure saved as {filename}.png")
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)
1017
1107
 
1018
- def on_joint_slider_changed(self, value):
1019
- """
1020
- Called when any joint slider is changed.
1021
- Updates the joint connection parameters and refreshes the plot.
1022
- """
1023
- num_of_factors = self.mechanism.factorizations[0].number_of_factors
1024
- # Update first factorization's linkage parameters.
1025
- for i in range(num_of_factors):
1026
- self.mechanism.factorizations[0].linkage[i].set_point_by_param(
1027
- 0, self.joint_sliders[2 * i].value() / 100.0)
1028
- self.mechanism.factorizations[0].linkage[i].set_point_by_param(
1029
- 1, self.joint_sliders[2 * i + 1].value() / 100.0)
1030
- # Update second factorization's linkage parameters.
1031
- for i in range(num_of_factors):
1032
- self.mechanism.factorizations[1].linkage[i].set_point_by_param(
1033
- 0, self.joint_sliders[2 * num_of_factors + 2 * i].value() / 100.0)
1034
- self.mechanism.factorizations[1].linkage[i].set_point_by_param(
1035
- 1, self.joint_sliders[2 * num_of_factors + 1 + 2 * i].value() / 100.0)
1036
- self.plot_slider_update(self.move_slider.value() / 100.0)
1037
-
1038
- def plot_slider_update(self, angle, t_param=None):
1039
- """
1040
- Update the mechanism plot based on the current joint angle or t parameter.
1041
- """
1042
- if t_param is not None:
1043
- t = t_param
1044
- else:
1045
- t = self.mechanism.factorizations[0].joint_angle_to_t_param(angle)
1046
-
1047
- # Compute link positions.
1048
- links = (self.mechanism.factorizations[0].direct_kinematics(t) +
1049
- self.mechanism.factorizations[1].direct_kinematics(t)[::-1])
1050
- links.insert(0, links[-1])
1051
-
1052
- if self.base is not None:
1053
- # Transform the links by the base transformation.
1054
- links = [self.base_arr @ np.insert(p, 0, 1) for p in links]
1055
- # Normalize the homogeneous coordinates.
1056
- links = [p[1:4] / p[0] for p in links]
1057
-
1058
- # Update each line segment.
1059
- for i, line in enumerate(self.lines):
1060
- pt1 = links[i]
1061
- pt2 = links[i+1]
1062
- pts = np.array([pt1, pt2])
1063
- line.setData(pos=pts)
1064
-
1065
- if self.show_tool:
1066
- pts0 = self.mechanism.factorizations[0].direct_kinematics(t)[-1]
1067
- pts1 = self.mechanism.factorizations[0].direct_kinematics_of_tool(
1068
- t, self.mechanism.tool_frame.dq2point_via_matrix())
1069
- pts2 = self.mechanism.factorizations[1].direct_kinematics(t)[-1]
1070
-
1071
- 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])
1072
1112
 
1073
1113
  if self.base is not None:
1074
- # Transform the tool triangle by the base transformation.
1075
- tool_triangle = [self.base_arr @ np.insert(p, 0, 1)
1076
- 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]
1077
1116
  # Normalize the homogeneous coordinates.
1078
- tool_triangle = [p[1:4] / p[0] for p in tool_triangle]
1079
-
1080
- self.tool_link.setData(pos=np.array(tool_triangle))
1081
-
1082
- # Update tool frame (pose) arrows.
1083
- pose_dq = DualQuaternion(self.mechanism.evaluate(t))
1084
- # Compute the pose matrix by composing the mechanism’s pose and tool frame.
1085
- pose_matrix = TransfMatrix(pose_dq.dq2matrix()) * TransfMatrix(
1086
- self.mechanism.tool_frame.dq2matrix())
1087
-
1088
- if self.base is not None:
1089
- # Transform the pose matrix by the base transformation.
1090
- pose_matrix = self.base * pose_matrix
1091
-
1092
- self.tool_frame.setData(pose_matrix)
1093
-
1094
- self.plotter.widget.update()
1095
-
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
1096
1158
 
1097
1159
  class InteractivePlotter:
1098
1160
  """