class GraphicsCanvas2D: """ Set up the scene with initial conditions. - White background - Width, height - Title, caption - Axes drawn (if applicable) :param height: Height of the canvas on screen (Pixels), defaults to 500. :type height: `int`, optional :param width: Width of the canvas on screen (Pixels), defaults to 1000. :type width: `int`, optional :param title: Title of the plot. Gets displayed above canvas, defaults to ''. :type title: `str`, optional :param caption: Caption (subtitle) of the plot. Gets displayed below the canvas, defaults to ''. :type caption: `str`, optional :param grid: Whether a grid should be displayed in the plot, defaults to `True`. :type grid: `bool`, optional """ def __init__(self, height=500, width=1000, title='', caption='', grid=True): # Create a new independent scene self.scene = canvas() # Apply the settings self.scene.background = color.white self.scene.width = width self.scene.height = height self.scene.autoscale = False # Disable default controls self.scene.userpan = False # Remove shift+mouse panning (key overwritten) self.scene.userzoom = True # Keep zoom controls (scrollwheel) self.scene.userspin = False # Remove ctrl+mouse enabled to rotate self.__grid_visibility = grid self.__camera_lock = False # Apply HTML title/caption if title != '': self.scene.title = title self.__default_caption = caption if caption != '': self.scene.caption = caption # Rotate the camera # convert_grid_to_z_up(self.scene) # Any time a key or mouse is held down, run the callback function rate(30) # 30Hz self.scene.bind('keydown', self.__handle_keyboard_inputs) # Create the grid, and display if wanted self.__graphics_grid = GraphicsGrid(self.scene) # Toggle grid to 2D self.__graphics_grid.toggle_2d_3d() # Lock the grid self.__graphics_grid.set_relative(False) # Turn off grid if applicable if not self.__grid_visibility: self.__graphics_grid.set_visibility(False) # Reset the camera to known spot self.__reset_camera() self.__graphics_grid.update_grid() ############################ # Canvas Management ############################ # TODO def clear_scene(self): pass def grid_visibility(self, is_visible): """ Update the grid visibility in the scene :param is_visible: Whether the grid should be visible or not :type is_visible: `bool` """ self.__graphics_grid.set_visibility(is_visible) ############################ # UI Management ############################ # TODO def __setup_ui_controls(self): pass def __handle_keyboard_inputs(self): """ Pans amount dependent on distance between camera and focus point. Closer = smaller pan amount A = move left (pan) D = move right (pan) W = move forward (pan) S = move backward (pan) <- = rotate left along camera axes (rotate) -> = rotate right along camera axes (rotate) ^ = rotate up along camera axes (rotate) V = rotate down along camera axes (rotate) Q = roll left (rotate) E = roll right (rotate) space = move up (pan) shift = move down (pan) ctrl + LMB = rotate (Default Vpython) """ # If camera lock, just skip the function if self.__camera_lock: return # Constants pan_amount = 0.02 # units rot_amount = 1.0 # deg # Current settings cam_distance = self.scene.camera.axis.mag cam_pos = vector(self.scene.camera.pos) cam_focus = vector(self.scene.center) # Weird manipulation to get correct vector directions. (scene.camera.up always defaults to world up) cam_axis = (vector(self.scene.camera.axis)) # X cam_side_axis = self.scene.camera.up.cross(cam_axis) # Y cam_up = cam_axis.cross(cam_side_axis) # Z cam_up.mag = cam_axis.mag # Get a list of keys keys = keysdown() # Userspin uses ctrl, so skip this check to avoid changing camera pose while ctrl is held if 'ctrl' in keys: return ################################################################################################################ # PANNING # Check if the keys are pressed, update vectors as required # Changing camera position updates the scene center to follow same changes if 'w' in keys: cam_pos = cam_pos + cam_axis * pan_amount if 's' in keys: cam_pos = cam_pos - cam_axis * pan_amount if 'a' in keys: cam_pos = cam_pos + cam_side_axis * pan_amount if 'd' in keys: cam_pos = cam_pos - cam_side_axis * pan_amount if ' ' in keys: cam_pos = cam_pos + cam_up * pan_amount if 'shift' in keys: cam_pos = cam_pos - cam_up * pan_amount # Update camera position before rotation (to keep pan and rotate separate) self.scene.camera.pos = cam_pos ################################################################################################################ # Camera Roll # If only one rotation key is pressed if 'q' in keys and 'e' not in keys: # Rotate camera up cam_up = cam_up.rotate(angle=-radians(rot_amount), axis=cam_axis) # Set magnitude as it went to inf cam_up.mag = cam_axis.mag # Set self.scene.up = cam_up # If only one rotation key is pressed if 'e' in keys and 'q' not in keys: # Rotate camera up cam_up = cam_up.rotate(angle=radians(rot_amount), axis=cam_axis) # Set magnitude as it went to inf cam_up.mag = cam_axis.mag # Set self.scene.up = cam_up ################################################################################################################ # CAMERA ROTATION d = cam_distance move_dist = sqrt(d**2 + d**2 - 2 * d * d * cos(radians(rot_amount))) # SAS Cosine # If only left not right key if 'left' in keys and 'right' not in keys: # Calculate distance to translate cam_pos = cam_pos + norm(cam_side_axis) * move_dist # Calculate new camera axis cam_axis = -(cam_pos - cam_focus) if 'right' in keys and 'left' not in keys: cam_pos = cam_pos - norm(cam_side_axis) * move_dist cam_axis = -(cam_pos - cam_focus) if 'up' in keys and 'down' not in keys: cam_pos = cam_pos + norm(cam_up) * move_dist cam_axis = -(cam_pos - cam_focus) if 'down' in keys and 'up' not in keys: cam_pos = cam_pos - norm(cam_up) * move_dist cam_axis = -(cam_pos - cam_focus) # Update camera position and axis self.scene.camera.pos = cam_pos self.scene.camera.axis = cam_axis def __reset_camera(self): """ Reset the camera to a known position """ # Reset Camera self.scene.camera.pos = vector(5, 5, 12) # Hover above (5, 5, 0) # Ever so slightly off focus, to ensure grid is rendered in the right region # (if directly at, draws numbers wrong spots) self.scene.camera.axis = vector(-0.001, -0.001, -12) # Focus on (5, 5, 0) self.scene.up = y_axis_vector ############################ # Drawing Functions ############################ def draw_path(self, xy_path, colour=None, thickness=0.05): """ Draw a line from point to point in the 2D path :param xy_path: The path to draw on the canvas :type xy_path: `list` :param colour: RGB list to colour the line to :type colour: `list` :param thickness: Thickness of the line :type thickness: `float` """ # Default colour to black if colour is None: colour = [0, 0, 0] if colour[0] > 1.0 or colour[1] > 1.0 or colour[2] > 1.0 or \ colour[0] < 0.0 or colour[1] < 0.0 or colour[2] < 0.0: raise ValueError("RGB values must be normalised between 0 and 1") # For every point in the list, draw a line to the next one (excluding last point) for point in range(0, len(xy_path) - 1): x1 = xy_path[point][0] y1 = xy_path[point][1] p1 = vector(x1, y1, 0) x2 = xy_path[point + 1][0] y2 = xy_path[point + 1][1] p2 = vector(x2, y2, 0) create_line(p1, p2, self.scene, colour=colour, thickness=thickness)
class GraphicsCanvas3D: """ Set up the scene with initial conditions. - White background - Width, height - Title, caption - Axes drawn (if applicable) :param height: Height of the canvas on screen (Pixels), defaults to 360. :type height: `int`, optional :param width: Width of the canvas on screen (Pixels), defaults to 640. :type width: `int`, optional :param title: Title of the plot. Gets displayed above canvas, defaults to ''. :type title: `str`, optional :param caption: Caption (subtitle) of the plot. Gets displayed below the canvas, defaults to ''. :type caption: `str`, optional :param grid: Whether a grid should be displayed in the plot, defaults to `True`. :type grid: `bool`, optional """ def __init__(self, height=360, width=640, title='', caption='', grid=True): # Create a new independent scene self.scene = canvas() # Apply the settings self.scene.background = color.white self.scene.width = width self.scene.height = height self.scene.autoscale = False # Disable default controls self.scene.userpan = False # Remove shift+mouse panning (not very good controls) self.scene.userzoom = True # Keep zoom controls (scrollwheel) self.scene.userspin = True # Keep ctrl+mouse enabled to rotate (keyboard rotation more tedious) # Apply HTML title/caption if title != '': self.scene.title = title self.__default_caption = caption if caption != '': self.scene.caption = caption # List of robots currently in the scene self.__robots = [] self.__selected_robot = 0 # List of joint sliders per robot self.__teachpanel = [] # 3D, robot -> joint -> options self.__teachpanel_sliders = [] self.__idx_qlim_min, self.__idx_qlim_max, self.__idx_theta = 0, 1, 2 # Checkbox states self.__grid_visibility = grid self.__camera_lock = False self.__grid_relative = True # Create the UI self.__ui_mode = UImode.CANVASCONTROL self.__toggle_button = None self.__toggle_button_text_dict = { UImode.CANVASCONTROL: "Canvas Controls", UImode.TEACHPANEL: "Robot Controls" } self.__ui_controls = self.__setup_ui_controls([]) # Indices to easily identify entities self.__idx_btn_reset = 0 # Camera Reset Button self.__idx_menu_robots = 1 # Menu box self.__idx_chkbox_ref = 2 # Reference Visibility Checkbox self.__idx_chkbox_rob = 3 # Robot Visibility Checkbox self.__idx_chkbox_grid = 4 # Grid Visibility Checkbox self.__idx_chkbox_cam = 5 # Camera Lock Checkbox self.__idx_chkbox_rel = 6 # Grid Relative Checkbox self.__idx_sld_opc = 7 # Opacity Slider self.__idx_btn_del = 8 # Delete button self.__idx_btn_clr = 9 # Clear button # Rotate the camera convert_grid_to_z_up(self.scene) # Any time a key or mouse is held down, run the callback function rate(30) # 30Hz self.scene.bind('keydown', self.__handle_keyboard_inputs) # Create the grid, and display if wanted self.__graphics_grid = GraphicsGrid(self.scene) if not self.__grid_visibility: self.__graphics_grid.set_visibility(False) ####################################### # Canvas Management ####################################### def clear_scene(self): """ This function will clear the screen of all objects """ # self.__graphics_grid.clear_scene() # Set all robots variables as invisible for robot in self.__robots: robot.set_reference_visibility(False) robot.set_robot_visibility(False) self.scene.waitfor("draw_complete") new_list = [] for name in self.__ui_controls[self.__idx_menu_robots].choices: new_list.append(name) self.__selected_robot = 0 self.__reload_caption(new_list) def grid_visibility(self, is_visible): """ Update the grid visibility in the scene :param is_visible: Whether the grid should be visible or not :type is_visible: `bool` """ self.__graphics_grid.set_visibility(is_visible) def add_robot(self, robot): """ This function is called when a new robot is created. It adds it to the drop down menu. :param robot: A graphical robot to add to the scene :type robot: class:`graphics.graphics_robot.GraphicalRobot` """ # ALTHOUGH THE DOCUMENTATION SAYS THAT MENU CHOICES CAN BE UPDATED, # THE PACKAGE DOES NOT ALLOW IT. # THUS THIS 'HACK' MUST BE DONE TO REFRESH THE UI WITH AN UPDATED LIST # Save the list of robot names new_list = [] for name in self.__ui_controls[self.__idx_menu_robots].choices: new_list.append(name) # Add the new one new_list.append(robot.name) # Add robot to list self.__robots.append(robot) self.__selected_robot = len(self.__robots) - 1 num_options = 3 self.__teachpanel.append([[0] * num_options] * robot.num_joints) # Add spot for current robot settings # Add robot joint sliders i = 0 for joint in robot.joints: self.__teachpanel[self.__selected_robot][i] = [joint.qlim[0], joint.qlim[1], joint.theta] i += 1 # Refresh the caption self.__reload_caption(new_list) # Set it as selected self.__ui_controls[self.__idx_menu_robots].index = len(self.__robots) - 1 ####################################### # UI Management ####################################### def __add_mode_button(self): """ Adds a button to the UI that toggles the UI mode :returns: A button :rtype: class:`vpython.button` """ btn_text = self.__toggle_button_text_dict.get(self.__ui_mode, "Unknown Mode Set") self.scene.append_to_caption('\n') btn_toggle = button(bind=self.__toggle_mode, text=btn_text) self.scene.append_to_caption('\n\n') return btn_toggle def __del_robot(self): """ Remove a robot from the scene and the UI controls """ if len(self.__robots) == 0: # Alert the user and return self.scene.append_to_caption('<script type="text/javascript">alert("No robot to delete");</script>') return # Clear the robot visuals self.__robots[self.__selected_robot].set_reference_visibility(False) self.__robots[self.__selected_robot].set_robot_visibility(False) # Remove from UI new_list = [] for name in self.__ui_controls[self.__idx_menu_robots].choices: new_list.append(name) del new_list[self.__selected_robot] del self.__robots[self.__selected_robot] del self.__teachpanel[self.__selected_robot] self.__selected_robot = 0 # Update UI self.__reload_caption(new_list) # Select the top item if len(self.__ui_controls[self.__idx_menu_robots].choices) > 0: self.__ui_controls[self.__idx_menu_robots].index = 0 def __handle_keyboard_inputs(self): """ Pans amount dependent on distance between camera and focus point. Closer = smaller pan amount A = move left (pan) D = move right (pan) W = move forward (pan) S = move backward (pan) <- = rotate left along camera axes (rotate) -> = rotate right along camera axes (rotate) ^ = rotate up along camera axes (rotate) V = rotate down along camera axes (rotate) Q = roll left (rotate) E = roll right (rotate) space = move up (pan) shift = move down (pan) ctrl + LMB = rotate (Default Vpython) """ # If camera lock, just skip the function if self.__camera_lock: return # Constants pan_amount = 0.02 # units rot_amount = 1.0 # deg # Current settings cam_distance = self.scene.camera.axis.mag cam_pos = vector(self.scene.camera.pos) cam_focus = vector(self.scene.center) # Weird manipulation to get correct vector directions. (scene.camera.up always defaults to world up) cam_axis = (vector(self.scene.camera.axis)) # X cam_side_axis = self.scene.camera.up.cross(cam_axis) # Y cam_up = cam_axis.cross(cam_side_axis) # Z cam_up.mag = cam_axis.mag # Get a list of keys keys = keysdown() # Userspin uses ctrl, so skip this check to avoid changing camera pose while ctrl is held if 'ctrl' in keys: return ################################################################################################################ # PANNING # Check if the keys are pressed, update vectors as required # Changing camera position updates the scene center to follow same changes if 'w' in keys: cam_pos = cam_pos + cam_axis * pan_amount if 's' in keys: cam_pos = cam_pos - cam_axis * pan_amount if 'a' in keys: cam_pos = cam_pos + cam_side_axis * pan_amount if 'd' in keys: cam_pos = cam_pos - cam_side_axis * pan_amount if ' ' in keys: cam_pos = cam_pos + cam_up * pan_amount if 'shift' in keys: cam_pos = cam_pos - cam_up * pan_amount # Update camera position before rotation (to keep pan and rotate separate) self.scene.camera.pos = cam_pos ################################################################################################################ # Camera Roll # If only one rotation key is pressed if 'q' in keys and 'e' not in keys: # Rotate camera up cam_up = cam_up.rotate(angle=-radians(rot_amount), axis=cam_axis) # Set magnitude as it went to inf cam_up.mag = cam_axis.mag # Set self.scene.up = cam_up # If only one rotation key is pressed if 'e' in keys and 'q' not in keys: # Rotate camera up cam_up = cam_up.rotate(angle=radians(rot_amount), axis=cam_axis) # Set magnitude as it went to inf cam_up.mag = cam_axis.mag # Set self.scene.up = cam_up ################################################################################################################ # CAMERA ROTATION d = cam_distance move_dist = sqrt(d ** 2 + d ** 2 - 2 * d * d * cos(radians(rot_amount))) # SAS Cosine # If only left not right key if 'left' in keys and 'right' not in keys: # Calculate distance to translate cam_pos = cam_pos + norm(cam_side_axis) * move_dist # Calculate new camera axis cam_axis = -(cam_pos - cam_focus) if 'right' in keys and 'left' not in keys: cam_pos = cam_pos - norm(cam_side_axis) * move_dist cam_axis = -(cam_pos - cam_focus) if 'up' in keys and 'down' not in keys: cam_pos = cam_pos + norm(cam_up) * move_dist cam_axis = -(cam_pos - cam_focus) if 'down' in keys and 'up' not in keys: cam_pos = cam_pos - norm(cam_up) * move_dist cam_axis = -(cam_pos - cam_focus) # Update camera position and axis self.scene.camera.pos = cam_pos self.scene.camera.axis = cam_axis def __reload_caption(self, new_list): """ Reload the UI with the new list of robot names """ # Remove all UI elements for item in self.__ui_controls: item.delete() for item in self.__teachpanel_sliders: item.delete() self.__teachpanel_sliders = [] # Restore the caption self.scene.caption = self.__default_caption # Create the updated caption. self.__toggle_button = self.__add_mode_button() self.__load_mode_ui(new_list) def __load_mode_ui(self, new_list): """ """ if self.__ui_mode == UImode.CANVASCONTROL: self.__ui_controls = self.__setup_ui_controls(new_list) elif self.__ui_mode == UImode.TEACHPANEL: self.__setup_joint_sliders() else: self.scene.append_to_caption("UNKNOWN MODE ENTERED\n") def __setup_ui_controls(self, list_of_names): """ The initial configuration of the user interface :param list_of_names: A list of names of the robots in the screen :type list_of_names: `list` """ # Button to reset camera btn_reset = button(bind=self.__reset_camera, text="Reset Camera") self.scene.append_to_caption('\t') chkbox_cam = checkbox(bind=self.__camera_lock_checkbox, text="Camera Lock", checked=self.__camera_lock) self.scene.append_to_caption('\t') chkbox_rel = checkbox(bind=self.__grid_relative_checkbox, text="Grid Relative", checked=self.__grid_relative) self.scene.append_to_caption('\n\n') # Drop down for robots / joints in frame menu_robots = menu(bind=self.__menu_item_chosen, choices=list_of_names) if not len(list_of_names) == 0: menu_robots.index = self.__selected_robot self.scene.append_to_caption('\t') # Button to delete the selected robot btn_del = button(bind=self.__del_robot, text="Delete Robot") self.scene.append_to_caption('\t') # Button to clear the robots in screen btn_clr = button(bind=self.clear_scene, text="Clear Scene") self.scene.append_to_caption('\n\n') # Checkbox for grid visibility chkbox_grid = checkbox(bind=self.__grid_visibility_checkbox, text="Grid Visibility", checked=self.__grid_visibility) self.scene.append_to_caption('\t') # Checkbox for reference frame visibilities if len(self.__robots) == 0: chkbox_ref = checkbox(bind=self.__reference_frame_checkbox, text="Show Reference Frames", checked=True) else: chk = self.__robots[self.__selected_robot].ref_shown chkbox_ref = checkbox(bind=self.__reference_frame_checkbox, text="Show Reference Frames", checked=chk) self.scene.append_to_caption('\t') # Checkbox for robot visibility if len(self.__robots) == 0: chkbox_rob = checkbox(bind=self.__robot_visibility_checkbox, text="Show Robot", checked=True) else: chk = self.__robots[self.__selected_robot].rob_shown chkbox_rob = checkbox(bind=self.__robot_visibility_checkbox, text="Show Robot", checked=chk) self.scene.append_to_caption('\n\n') # Slider for robot opacity self.scene.append_to_caption('Opacity:') if len(self.__robots) == 0: sld_opc = slider(bind=self.__opacity_slider, value=1) else: opc = self.__robots[self.__selected_robot].opacity sld_opc = slider(bind=self.__opacity_slider, value=opc) # self.scene.append_to_caption('\n\n') # Prevent the space bar from toggling the active checkbox/button/etc (default browser behaviour) self.scene.append_to_caption(''' <script type="text/javascript"> $(document).keyup(function(event) { if(event.which === 32) { event.preventDefault(); } }); </script>''') # https://stackoverflow.com/questions/22280139/prevent-space-button-from-triggering-any-other-button-click-in-jquery # Control manual controls_str = '<br><b>Controls</b><br>' \ '<b>PAN</b><br>' \ 'W , S | <i>forward / backward</i><br>' \ 'A , D | <i>left / right</i><br>' \ 'SPACE , SHIFT | <i>up / down</i><br>' \ '<b>ROTATE</b><br>' \ 'CTRL + LMB | <i>free spin</i><br>' \ 'ARROWS KEYS | <i>rotate direction</i><br>' \ 'Q , E | <i>roll left / right</i><br>' \ '<b>ZOOM</b></br>' \ 'MOUSEWHEEL | <i>zoom in / out</i><br>' \ '<script type="text/javascript">var arrow_keys_handler = function(e) {switch(e.keyCode){ case 37: case 39: case 38: case 40: case 32: e.preventDefault(); break; default: break;}};window.addEventListener("keydown", arrow_keys_handler, false);</script>' # Disable the arrow keys from scrolling in the browser # https://stackoverflow.com/questions/8916620/disable-arrow-key-scrolling-in-users-browser self.scene.append_to_caption(controls_str) return [btn_reset, menu_robots, chkbox_ref, chkbox_rob, chkbox_grid, chkbox_cam, chkbox_rel, sld_opc, btn_del, btn_clr] def __setup_joint_sliders(self): """ Display the Teachpanel mode of the UI """ i = 1 for joint in self.__teachpanel[self.__selected_robot]: # Add a title self.scene.append_to_caption('Joint {0}:\t'.format(i)) i += 1 # Add the slider, with the correct joint variables s = slider( bind=self.__joint_slider, min=joint[self.__idx_qlim_min], max=joint[self.__idx_qlim_max], value=joint[self.__idx_theta] ) self.__teachpanel_sliders.append(s) self.scene.append_to_caption('\n\n') ####################################### # UI CALLBACKS ####################################### def __toggle_mode(self): """ Callback for when the toggle mode button is pressed """ # Update mode self.__ui_mode = { UImode.CANVASCONTROL: UImode.TEACHPANEL, UImode.TEACHPANEL: UImode.CANVASCONTROL }.get(self.__ui_mode, UImode.CANVASCONTROL) # Update mode, default canvas controls # Update UI # get list of robots new_list = [] for name in self.__ui_controls[self.__idx_menu_robots].choices: new_list.append(name) self.__reload_caption(new_list) def __reset_camera(self): """ Reset the camera to a default position and orientation """ # Reset Camera self.scene.up = z_axis_vector self.scene.camera.pos = vector(10, 10, 10) self.scene.camera.axis = -self.scene.camera.pos # Update grid self.__graphics_grid.update_grid() def __menu_item_chosen(self, m): """ When a menu item is chosen, update the relevant checkboxes/options :param m: The menu object that has been used to select an item. :type: class:`menu` """ # Get selected item self.__selected_robot = m.index # Update the checkboxes/sliders for the selected robot self.__ui_controls[self.__idx_chkbox_ref].checked = \ self.__robots[self.__selected_robot].ref_shown self.__ui_controls[self.__idx_chkbox_rob].checked = \ self.__robots[self.__selected_robot].rob_shown self.__ui_controls[self.__idx_sld_opc].value = \ self.__robots[self.__selected_robot].opacity def __reference_frame_checkbox(self, c): """ When a checkbox is changed for the reference frame option, update the graphics :param c: The checkbox that has been toggled :type c: class:`checkbox` """ if len(self.__robots) > 0: self.__robots[self.__selected_robot].set_reference_visibility(c.checked) def __robot_visibility_checkbox(self, c): """ When a checkbox is changed for the robot visibility, update the graphics :param c: The checkbox that has been toggled :type c: class:`checkbox` """ if len(self.__robots) > 0: self.__robots[self.__selected_robot].set_robot_visibility(c.checked) def __grid_visibility_checkbox(self, c): """ When a checkbox is changed for the grid visibility, update the graphics :param c: The checkbox that has been toggled :type c: class:`checkbox` """ self.grid_visibility(c.checked) self.__grid_visibility = c.checked def __camera_lock_checkbox(self, c): """ When a checkbox is changed for the camera lock, update the camera :param c: The checkbox that has been toggled :type c: class:`checkbox` """ # Update parameters # True = locked self.__camera_lock = c.checked # True = enabled self.scene.userspin = not c.checked self.scene.userzoom = not c.checked def __grid_relative_checkbox(self, c): """ When a checkbox is changed for the grid lock, update the grid :param c: The checkbox that has been toggled :type c: class:`checkbox` """ self.__graphics_grid.set_relative(c.checked) self.__grid_relative = c.checked def __opacity_slider(self, s): """ Update the opacity slider depending on the slider value :param s: The slider object that has been modified :type s: class:`slider` """ if len(self.__robots) > 0: self.__robots[self.__selected_robot].set_transparency(s.value) def __joint_slider(self, s): """ The callback for when a joint slider has changed value :param s: The slider object that has been modified :type s: class:`slider` """ # Save the values for updating later for slider_num in range(0, len(self.__teachpanel_sliders)): self.__teachpanel[self.__selected_robot][slider_num][self.__idx_theta] = \ self.__teachpanel_sliders[slider_num].value # Get all angles for the robot angles = [] for joint_slider in self.__teachpanel_sliders: angles.append(joint_slider.value) # Run fkine poses = self.__robots[self.__selected_robot].fkine(angles) poses = poses[1:len(poses)] # Ignore the first item # Update joints self.__robots[self.__selected_robot].set_joint_poses(poses)
class GraphicsCanvas2D: """ Set up the scene with initial conditions. - White background - Width, height - Title, caption - Axes drawn (if applicable) :param height: Height of the canvas on screen (Pixels), defaults to 500. :type height: `int`, optional :param width: Width of the canvas on screen (Pixels), defaults to 1000. :type width: `int`, optional :param title: Title of the plot. Gets displayed above canvas, defaults to ''. :type title: `str`, optional :param caption: Caption (subtitle) of the plot. Gets displayed below the canvas, defaults to ''. :type caption: `str`, optional :param grid: Whether a grid should be displayed in the plot, defaults to `True`. :type grid: `bool`, optional """ def __init__(self, height=500, width=1000, title='', caption='', grid=True): # Private lists self.__line_styles = [ '', # None '-', # Solid (default) '--', # Dashes ':', # Dotted '-.', # Dash-dot ] self.__marker_styles = [ '+', # Plus 'o', # Circle '*', # Star '.', # Dot 'x', # Cross 's', # Square 'd', # Diamond '^', # Up triangle 'v', # Down triangle '<', # Left triangle '>', # Right triangle 'p', # Pentagon 'h', # Hexagon ] self.__colour_styles = [ 'r', # Red 'g', # Green 'b', # Blue 'y', # Yellow 'c', # Cyan 'm', # Magenta 'k', # Black (default) 'w', # White ] self.__colour_dictionary = { 'r': color.red.value, 'g': color.green.value, 'b': color.blue.value, 'c': color.cyan.value, 'y': color.yellow.value, 'm': color.magenta.value, 'k': color.black.value, 'w': color.white.value } # Create a new independent scene self.scene = canvas() # Apply the settings self.scene.background = color.white self.scene.width = width self.scene.height = height self.scene.autoscale = False # Disable default controls self.scene.userpan = True # Keep shift+mouse panning (key overwritten) self.scene.userzoom = True # Keep zoom controls (scrollwheel) self.scene.userspin = False # Remove ctrl+mouse enabled to rotate self.__grid_visibility = grid self.__camera_lock = False # Apply HTML title/caption if title != '': self.scene.title = title self.__default_caption = caption if caption != '': self.scene.caption = caption # Rotate the camera # convert_grid_to_z_up(self.scene) # Any time a key or mouse is held down, run the callback function rate(30) # 30Hz self.scene.bind('keydown', self.__handle_keyboard_inputs) # Create the grid, and display if wanted self.__graphics_grid = GraphicsGrid(self.scene) # Toggle grid to 2D self.__graphics_grid.toggle_2d_3d() # Lock the grid self.__graphics_grid.set_relative(False) # Turn off grid if applicable if not self.__grid_visibility: self.__graphics_grid.set_visibility(False) # Reset the camera to known spot self.__reset_camera() self.__graphics_grid.update_grid() ####################################### # Canvas Management ####################################### # TODO def clear_scene(self): pass def grid_visibility(self, is_visible): """ Update the grid visibility in the scene :param is_visible: Whether the grid should be visible or not :type is_visible: `bool` """ self.__graphics_grid.set_visibility(is_visible) ####################################### # UI Management ####################################### # TODO def __setup_ui_controls(self): pass def __handle_keyboard_inputs(self): """ Pans amount dependent on distance between camera and focus point. Closer = smaller pan amount A = move left (pan) D = move right (pan) W = move up (pan) S = move down (pan) <- = rotate left along camera axes (rotate) -> = rotate right along camera axes (rotate) ^ = rotate up along camera axes (rotate) V = rotate down along camera axes (rotate) Q = roll left (rotate) E = roll right (rotate) ctrl + LMB = rotate (Default Vpython) """ # If camera lock, just skip the function if self.__camera_lock: return # Constants pan_amount = 0.02 # units rot_amount = 1.0 # deg # Current settings cam_distance = self.scene.camera.axis.mag cam_pos = vector(self.scene.camera.pos) cam_focus = vector(self.scene.center) # Weird manipulation to get correct vector directions. (scene.camera.up always defaults to world up) cam_axis = (vector(self.scene.camera.axis)) # X cam_side_axis = self.scene.camera.up.cross(cam_axis) # Y cam_up = cam_axis.cross(cam_side_axis) # Z cam_up.mag = cam_axis.mag # Get a list of keys keys = keysdown() # Userpan uses ctrl, so skip this check to avoid changing camera pose while shift is held if 'shift' in keys: return ################################################################################################################ # PANNING # Check if the keys are pressed, update vectors as required # Changing camera position updates the scene center to follow same changes if 'w' in keys: cam_pos = cam_pos + cam_up * pan_amount if 's' in keys: cam_pos = cam_pos - cam_up * pan_amount if 'a' in keys: cam_pos = cam_pos + cam_side_axis * pan_amount if 'd' in keys: cam_pos = cam_pos - cam_side_axis * pan_amount # Update camera position before rotation (to keep pan and rotate separate) self.scene.camera.pos = cam_pos ################################################################################################################ # Camera Roll # If only one rotation key is pressed if 'q' in keys and 'e' not in keys: # Rotate camera up cam_up = cam_up.rotate(angle=-radians(rot_amount), axis=cam_axis) # Set magnitude as it went to inf cam_up.mag = cam_axis.mag # Set self.scene.up = cam_up # If only one rotation key is pressed if 'e' in keys and 'q' not in keys: # Rotate camera up cam_up = cam_up.rotate(angle=radians(rot_amount), axis=cam_axis) # Set magnitude as it went to inf cam_up.mag = cam_axis.mag # Set self.scene.up = cam_up ################################################################################################################ # CAMERA ROTATION d = cam_distance move_dist = sqrt(d**2 + d**2 - 2 * d * d * cos(radians(rot_amount))) # SAS Cosine # If only left not right key if 'left' in keys and 'right' not in keys: # Calculate distance to translate cam_pos = cam_pos + norm(cam_side_axis) * move_dist # Calculate new camera axis cam_axis = -(cam_pos - cam_focus) if 'right' in keys and 'left' not in keys: cam_pos = cam_pos - norm(cam_side_axis) * move_dist cam_axis = -(cam_pos - cam_focus) if 'up' in keys and 'down' not in keys: cam_pos = cam_pos + norm(cam_up) * move_dist cam_axis = -(cam_pos - cam_focus) if 'down' in keys and 'up' not in keys: cam_pos = cam_pos - norm(cam_up) * move_dist cam_axis = -(cam_pos - cam_focus) # Update camera position and axis self.scene.camera.pos = cam_pos self.scene.camera.axis = cam_axis def __reset_camera(self): """ Reset the camera to a known position """ # Reset Camera self.scene.camera.pos = vector(5, 5, 12) # Hover above (5, 5, 0) # Ever so slightly off focus, to ensure grid is rendered in the right region # (if directly at, draws numbers wrong spots) self.scene.camera.axis = vector(-0.001, -0.001, -12) # Focus on (5, 5, 0) self.scene.up = y_axis_vector ####################################### # Drawing Functions ####################################### def __draw_path(self, x_path, y_path, opt_line, opt_marker, opt_colour, thickness=0.05): """ Draw a line from point to point in the 2D path :param x_path: The x path to draw on the canvas :type x_path: `list` :param y_path: The y path to draw on the canvas :type y_path: `list` :param opt_line: The line option argument :type opt_line: `str` :param opt_marker: The marker option argument :type opt_marker: `str` :param opt_colour: The colour option argument :type opt_colour: `str` :param thickness: Thickness of the line :type thickness: `float` :raises ValueError: Invalid line type given """ # Get colour colour = self.__get_colour_from_string(opt_colour) # For every point in the list, draw a line to the next one (excluding last point) for point in range(0, len(x_path)): # Get point 1 x1 = x_path[point] y1 = y_path[point] p1 = vector(x1, y1, 0) # If at end / only coordinate - draw a marker if point == len(x_path) - 1: create_marker(self.scene, x1, y1, opt_marker, colour) return # Get point 2 x2 = x_path[point + 1] y2 = y_path[point + 1] p2 = vector(x2, y2, 0) if opt_line == '': # Only one marker to avoid double-ups create_marker(self.scene, x1, y1, opt_marker, colour) elif opt_line == '-': create_line(p1, p2, self.scene, colour=colour, thickness=thickness) # Only one marker to avoid double-ups create_marker(self.scene, x1, y1, opt_marker, colour) elif opt_line == '--': create_segmented_line(p1, p2, self.scene, 0.3, colour=colour, thickness=thickness) # Only one marker to avoid double-ups create_marker(self.scene, x1, y1, opt_marker, colour) elif opt_line == ':': create_segmented_line(p1, p2, self.scene, 0.05, colour=colour, thickness=thickness) # Only one marker to avoid double-ups create_marker(self.scene, x1, y1, opt_marker, colour) elif opt_line == '-.': raise NotImplementedError("Other line types not implemented") else: raise ValueError("Invalid line type given") def plot(self, x_coords, y_coords=None, options=''): """ Same usage as MATLAB's plot. If given one list of coordinates, plots against index If given two lists of coordinates, plots both (1st = x, 2nd = y) Options string is identical to MATLAB's input string If you do not specify a marker type, plot uses no marker. If you do not specify a line style, plot uses a solid line. b blue . point - solid g green o circle : dotted r red x x-mark -. dashdot c cyan + plus -- dashed m magenta * star (none) no line y yellow s square k black d diamond w white v triangle (down) ^ triangle (up) < triangle (left) > triangle (right) p pentagram h hexagram :param x_coords: The first plane of coordinates to plot :type x_coords: `list` :param y_coords: The second plane of coordinates to plot with. :type y_coords: `list`, `str`, optional :param options: A string of options to plot with :type options: `str`, optional :raises ValueError: Number of X and Y coordinates must be equal """ # TODO # add options for line width, marker size # If y-vector is str, then only x vector given if isinstance(y_coords, str): options = y_coords y_coords = None one_set_data = False # Set y-vector to default if None if y_coords is None: one_set_data = True y_coords = [*range(0, len(x_coords))] # Verify x, y coords have same length if len(x_coords) != len(y_coords): raise ValueError( "Number of X coordinates does not equal number of Y coordinates." ) # Verify options given (and save settings to be applied) verified_options = self.__verify_plot_options(options) if one_set_data: # Draw plot for one list of data self.__draw_path( y_coords, # Y is default x-coords in one data set x_coords, # User input verified_options[0], # Line verified_options[1], # Marker verified_options[2], # Colour ) else: # Draw plot for two lists of data self.__draw_path( x_coords, # User input y_coords, # User input verified_options[0], # Line verified_options[1], # Marker verified_options[2], # Colour ) def __verify_plot_options(self, options_str): """ Verify that the given options are usable. :param options_str: The given options from the plot command to verify user input :type options_str: `str` :raises ValueError: Unknown character entered :raises ValueError: Too many line segments used :raises ValueError: Too many marker segments used :raises ValueError: Too many colour segments used :returns: List of options to plot with :rtype: `list` """ default_line = '-' default_marker = '' default_colour = 'k' # Split str into chars list options_split = list(options_str) # If 0, set defaults and return early if len(options_split) == 0: return [default_line, default_marker, default_colour] # If line_style given, join the first two options if applicable (some types have 2 characters) for char in range(0, len(options_split) - 1): # If char is '-' (only leading character in double length option) if options_split[char] == '-' and len(options_split) > 1: # If one of the leading characters is valid if options_split[char + 1] == '-' or options_split[char + 1] == '.': # Join the two into the first options_split[char] = options_split[char] + options_split[ char + 1] # Shuffle down the rest for idx in range(char + 2, len(options_split)): options_split[idx - 1] = options_split[idx] # Remove duplicate extra options_split.pop() # If any unknown, throw error for option in options_split: if option not in self.__line_styles and \ option not in self.__marker_styles and \ option not in self.__colour_styles: error_string = "Unknown character entered: '{0}'" raise ValueError(error_string.format(option)) ############################## # Verify Line Style ############################## line_style_count = 0 # Count of options used line_style_index = 0 # Index position of index used (only used when count == 1) for option in options_split: if option in self.__line_styles: line_style_count = line_style_count + 1 line_style_index = self.__line_styles.index(option) # If more than one, throw error if line_style_count > 1: raise ValueError( "Too many line style arguments given. Only one allowed") # If none, set as solid elif line_style_count == 0 or not any(item in options_split for item in self.__line_styles): output_line = default_line # If one, set as given else: output_line = self.__line_styles[line_style_index] ############################## ############################## # Verify Marker Style ############################## marker_style_count = 0 # Count of options used marker_style_index = 0 # Index position of index used (only used when count == 1) for option in options_split: if option in self.__marker_styles: marker_style_count = marker_style_count + 1 marker_style_index = self.__marker_styles.index(option) # If more than one, throw error if marker_style_count > 1: raise ValueError( "Too many marker style arguments given. Only one allowed") # If none, set as no-marker elif marker_style_count == 0 or not any( item in options_split for item in self.__marker_styles): output_marker = default_marker # If one, set as given else: output_marker = self.__marker_styles[marker_style_index] # If marker set and no line given, turn line to no-line if line_style_count == 0 or not any( item in options_split for item in self.__line_styles): output_line = '' ############################## ############################## # Verify Colour Style ############################## colour_style_count = 0 # Count of options used colour_style_index = 0 # Index position of index used (only used when count == 1) for option in options_split: if option in self.__colour_styles: colour_style_count = colour_style_count + 1 colour_style_index = self.__colour_styles.index(option) # If more than one, throw error if colour_style_count > 1: raise ValueError( "Too many colour style arguments given. Only one allowed") # If none, set as black elif colour_style_count == 0 or not any( item in options_split for item in self.__colour_styles): output_colour = default_colour # If one, set as given else: output_colour = self.__colour_styles[colour_style_index] ############################## return [output_line, output_marker, output_colour] def __get_colour_from_string(self, colour_string): """ Using the colour plot string input, return an rgb array of the colour selected :param colour_string: The colour string option :type colour_string: `str` :returns: List of RGB values for the representative colour :rtype: `list` """ # Return the RGB list (black if not in dictionary) return self.__colour_dictionary.get(colour_string, color.black.value)