예제 #1
0
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 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):

        # 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)
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)